diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-10-26 14:09:52 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-11-04 20:03:24 +0000 |
commit | 8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e (patch) | |
tree | caec9feed43b55d82cdeff0fb7c9d5272434bca3 /server | |
parent | ad08e7f7912d9fed1f334803f7d71dc999fdbecd (diff) | |
download | sonarqube-8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e.tar.gz sonarqube-8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e.zip |
SONAR-15567 Add export UI to community edition
Diffstat (limited to 'server')
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; |