From 8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Tue, 26 Oct 2021 14:09:52 +0200 Subject: SONAR-15567 Add export UI to community edition --- server/sonar-web/src/main/js/api/project-dump.ts | 34 +++ .../main/js/app/components/nav/component/Menu.tsx | 11 + .../__tests__/__snapshots__/Menu-test.tsx.snap | 135 ++++++++++- .../src/main/js/app/utils/startReactApp.tsx | 2 + .../main/js/apps/projectDump/ProjectDumpApp.tsx | 200 +++++++++++++++++ .../projectDump/__tests__/ProjectDumpApp-test.tsx | 95 ++++++++ .../__snapshots__/ProjectDumpApp-test.tsx.snap | 140 ++++++++++++ .../main/js/apps/projectDump/components/Export.tsx | 188 ++++++++++++++++ .../main/js/apps/projectDump/components/Import.tsx | 180 +++++++++++++++ .../components/__tests__/Export-test.tsx | 62 ++++++ .../components/__tests__/Import-test.tsx | 71 ++++++ .../__tests__/__snapshots__/Export-test.tsx.snap | 206 +++++++++++++++++ .../__tests__/__snapshots__/Import-test.tsx.snap | 246 +++++++++++++++++++++ .../src/main/js/apps/projectDump/routes.ts | 28 +++ .../src/main/js/apps/projectDump/styles.css | 39 ++++ server/sonar-web/src/main/js/helpers/testMocks.ts | 22 ++ server/sonar-web/src/main/js/types/project-dump.ts | 34 +++ server/sonar-web/src/main/js/types/tasks.ts | 4 +- server/sonar-web/src/main/js/types/types.d.ts | 1 + 19 files changed, 1696 insertions(+), 2 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/project-dump.ts create mode 100644 server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectDump/routes.ts create mode 100644 server/sonar-web/src/main/js/apps/projectDump/styles.css create mode 100644 server/sonar-web/src/main/js/types/project-dump.ts (limited to 'server') 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 { + 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 { 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 { ); }; + renderImportExportLink = (query: Query) => { + return ( +
  • + + {translate('project_dump.page')} + +
  • + ); + }; + 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 @@ -349,6 +349,24 @@ exports[`should work for a branch 1`] = ` project_baseline.page +
  • + + project_dump.page + +
  • +
  • + + project_dump.page + +
  • +
  • + + project_dump.page + +
  • - + + +
  • + + project_dump.page + +
  • + + } + tagName="li" + > + +
    +
    `; @@ -1105,6 +1187,23 @@ exports[`should work for all qualifiers 4`] = ` application_console.page +
  • + + project_dump.page + +
  • +
  • + + project_dump.page + +
  • +
  • + + project_dump.page + +
  • + 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; + component: T.Component; +} + +interface State { + lastAnalysisTask?: DumpTask; + lastExportTask?: DumpTask; + lastImportTask?: DumpTask; + status?: DumpStatus; +} + +export class ProjectDumpApp extends React.Component { + 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 ( +
    +
    +

    {translate('project_dump.page')}

    +
    + {projectImportFeatureEnabled + ? translate('project_dump.page.description') + : translate('project_dump.page.description_without_import')} +
    +
    + + {status === undefined ? ( + + ) : ( +
    +
    + +
    +
    + +
    +
    + )} +
    + ); + } +} + +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 = {}) { + return shallow( + + ); +} 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`] = ` +
    +
    +

    + project_dump.page +

    +
    + project_dump.page.description +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +`; + +exports[`should render correctly: loaded without import 1`] = ` +
    +
    +

    + project_dump.page +

    +
    + project_dump.page.description_without_import +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +`; + +exports[`should render correctly: loading 1`] = ` +
    +
    +

    + project_dump.page +

    +
    + project_dump.page.description +
    +
    + +
    +`; 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 { + handleExport = () => { + doExport(this.props.componentKey).then(this.props.loadStatus, () => { + /* no catch needed */ + }); + }; + + renderHeader() { + return ( +
    +

    {translate('project_dump.export')}

    +
    + ); + } + + renderWhenCanNotExport() { + return ( +
    + {this.renderHeader()} +
    + + {translate('project_dump.can_not_export')} + +
    +
    + ); + } + + renderWhenExportPending(task: DumpTask) { + return ( +
    + {this.renderHeader()} +
    + + + {formatted => ( + {translateWithParameters('project_dump.pending_export', formatted)} + )} + +
    +
    + ); + } + + renderWhenExportInProgress(task: DumpTask) { + return ( +
    + {this.renderHeader()} + +
    + + {task.startedAt && ( + + {fromNow => ( + {translateWithParameters('project_dump.in_progress_export', fromNow)} + )} + + )} +
    +
    + ); + } + + renderWhenExportFailed() { + const { componentKey } = this.props; + const detailsUrl = `${getBaseUrl()}/project/background_tasks?id=${encodeURIComponent( + componentKey + )}&status=FAILED&taskType=PROJECT_EXPORT`; + + return ( +
    + {this.renderHeader()} + +
    + + {translate('project_dump.failed_export')} + + {translate('project_dump.see_details')} + + + + {this.renderExport()} +
    +
    + ); + } + + renderDump(task?: DumpTask) { + const { status } = this.props; + + return ( + + {task && task.executedAt && ( + + {formatted => ( +
    + {translateWithParameters('project_dump.latest_export_available', formatted)} +
    + )} +
    + )} + {!task && ( +
    {translate('project_dump.export_available')}
    + )} +
    + {status.exportedDump} +
    +
    + ); + } + + renderExport() { + return ( +
    +
    {translate('project_dump.export_form_description')}
    + +
    + ); + } + + 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 ( +
    + {this.renderHeader()} +
    + {isDumpAvailable && this.renderDump(task)} + {this.renderExport()} +
    +
    + ); + } +} 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 { + handleImport = () => { + doImport(this.props.componentKey).then(this.props.loadStatus, () => { + /* no catch needed */ + }); + }; + + renderWhenCanNotImport() { + return ( +
    + {translate('project_dump.can_not_import')} +
    + ); + } + + renderWhenNoDump() { + return ( +
    + + {translate('project_dump.no_file_to_import')} + +
    + ); + } + + renderImportForm() { + return ( +
    +
    {translate('project_dump.import_form_description')}
    + +
    + ); + } + + renderWhenImportSuccess(task: DumpTask) { + return ( +
    + {task.executedAt && ( + + {formatted => ( + + {translateWithParameters('project_dump.import_success', formatted)} + + )} + + )} +
    + ); + } + + renderWhenImportPending(task: DumpTask) { + return ( +
    + + + {formatted => ( + {translateWithParameters('project_dump.pending_import', formatted)} + )} + +
    + ); + } + + renderWhenImportInProgress(task: DumpTask) { + return ( +
    + + {task.startedAt && ( + + {fromNow => ( + {translateWithParameters('project_dump.in_progress_import', fromNow)} + )} + + )} +
    + ); + } + + renderWhenImportFailed() { + const { componentKey } = this.props; + const detailsUrl = `${getBaseUrl()}/project/background_tasks?${stringify({ + id: encodeURIComponent(componentKey), + status: TaskStatuses.Failed, + taskType: TaskTypes.ProjectImport + })}`; + + return ( +
    + + {translate('project_dump.failed_import')} + + {translate('project_dump.see_details')} + + + + {this.renderImportForm()} +
    + ); + } + + 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 =
    {this.renderImportForm()}
    ; + } + return ( +
    +
    +

    {translate('project_dump.import')}

    +
    + {importEnabled ? ( + content + ) : ( +
    + {translate('project_dump.import_form_description_disabled')} +
    + )} +
    + ); + } +} 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 = {}) { + return shallow( + + ); +} 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 = {}) { + return shallow( + + ); +} 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`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    + + project_dump.can_not_export + +
    +
    +`; + +exports[`should render correctly: no task 1`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    +
    +
    + project_dump.export_form_description +
    + +
    +
    +
    +`; + +exports[`should render correctly: success 1`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    + + + + +
    + + dump-file + +
    +
    +
    +
    + project_dump.export_form_description +
    + +
    +
    +
    +`; + +exports[`should render correctly: task failed 1`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    + + project_dump.failed_export + + project_dump.see_details + + +
    +
    + project_dump.export_form_description +
    + +
    +
    +
    +`; + +exports[`should render correctly: task in progress 1`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    + + + + +
    +
    +`; + +exports[`should render correctly: task pending 1`] = ` +
    +
    +

    + project_dump.export +

    +
    +
    + + + + +
    +
    +`; 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`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + project_dump.can_not_import +
    +
    +`; + +exports[`should render correctly: failed 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + project_dump.failed_import + + project_dump.see_details + + +
    +
    + project_dump.import_form_description +
    + +
    +
    +
    +`; + +exports[`should render correctly: import disabled 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + project_dump.import_form_description_disabled +
    +
    +`; + +exports[`should render correctly: import form 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    +
    +
    + project_dump.import_form_description +
    + +
    +
    +
    +`; + +exports[`should render correctly: in progress 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + + + +
    +
    +`; + +exports[`should render correctly: no dump to import 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + project_dump.no_file_to_import + +
    +
    +`; + +exports[`should render correctly: pending 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + + + +
    +
    +`; + +exports[`should render correctly: success 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + + +
    +
    +`; + +exports[`should render correctly: success, but with analysis -> show form 1`] = ` +
    +
    +

    + project_dump.import +

    +
    +
    + + project_dump.no_file_to_import + +
    +
    +`; 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 { return { @@ -755,3 +757,23 @@ export function mockPaging(overrides: Partial = {}): T.Paging { ...overrides }; } + +export function mockDumpTask(props: Partial = {}): 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 { + 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; -- cgit v1.2.3