* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- BasicSeparator,
- LargeCenteredLayout,
- PageContentFontWrapper,
- Spinner,
- Title,
-} from 'design-system';
+import { BasicSeparator, LargeCenteredLayout, PageContentFontWrapper, Title } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
-import { throwGlobalError } from '~sonar-aligned/helpers/error';
-import { getActivity } from '../../api/ce';
-import { getStatus } from '../../api/project-dump';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../app/components/available-features/withAvailableFeatures';
import withComponentContext from '../../app/components/componentContext/withComponentContext';
import { translate } from '../../helpers/l10n';
import { Feature } from '../../types/features';
-import { DumpStatus, DumpTask } from '../../types/project-dump';
-import { ActivityRequestParameters, TaskStatuses, TaskTypes } from '../../types/tasks';
import { Component } from '../../types/types';
import Export from './components/Export';
import Import from './components/Import';
import './styles.css';
-const POLL_INTERNAL = 5000;
-
interface Props extends WithAvailableFeaturesProps {
component: 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: ActivityRequestParameters = {
- type,
- component,
- ps: 1,
- 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 projectImportFeatureEnabled = this.props.hasFeature(Feature.ProjectImport);
- 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,
- }));
- }
+export function ProjectDumpApp({ component, hasFeature }: Readonly<Props>) {
+ const projectImportFeatureEnabled = hasFeature(Feature.ProjectImport);
- 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 } = this.props;
- const projectImportFeatureEnabled = this.props.hasFeature(Feature.ProjectImport);
- const { lastAnalysisTask, lastExportTask, lastImportTask, status } = this.state;
-
- return (
- <LargeCenteredLayout id="project-dump">
- <PageContentFontWrapper className="sw-my-8 sw-body-sm">
- <header className="sw-mb-5">
- <Helmet defer={false} title={translate('project_dump.page')} />
- <Title className="sw-mb-4">{translate('project_dump.page')}</Title>
- <div>
- {projectImportFeatureEnabled ? (
- <>
- <p>{translate('project_dump.page.description1')}</p>
- <p>{translate('project_dump.page.description2')}</p>
- </>
- ) : (
- <>
- <p>{translate('project_dump.page.description_without_import1')}</p>
- <p>{translate('project_dump.page.description_without_import2')}</p>
- </>
- )}
- </div>
- </header>
-
- <Spinner loading={status === undefined}>
- {status && (
+ return (
+ <LargeCenteredLayout id="project-dump">
+ <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+ <header className="sw-mb-5">
+ <Helmet defer={false} title={translate('project_dump.page')} />
+ <Title className="sw-mb-4">{translate('project_dump.page')}</Title>
+ <div>
+ {projectImportFeatureEnabled ? (
<>
- <Export
- componentKey={component.key}
- loadStatus={this.poll}
- status={status}
- task={lastExportTask}
- />
- <BasicSeparator className="sw-my-8" />
- <Import
- importEnabled={!!projectImportFeatureEnabled}
- analysis={lastAnalysisTask}
- componentKey={component.key}
- loadStatus={this.poll}
- status={status}
- task={lastImportTask}
- />
+ <p>{translate('project_dump.page.description1')}</p>
+ <p>{translate('project_dump.page.description2')}</p>
+ </>
+ ) : (
+ <>
+ <p>{translate('project_dump.page.description_without_import1')}</p>
+ <p>{translate('project_dump.page.description_without_import2')}</p>
</>
)}
- </Spinner>
- </PageContentFontWrapper>
- </LargeCenteredLayout>
- );
- }
+ </div>
+ </header>
+
+ <>
+ <div className="sw-mb-4">
+ <h2 className="sw-heading-md">{translate('project_dump.export')}</h2>
+ </div>
+ <Export componentKey={component.key} />
+ <BasicSeparator className="sw-my-8" />
+ <Import importEnabled={!!projectImportFeatureEnabled} componentKey={component.key} />
+ </>
+ </PageContentFontWrapper>
+ </LargeCenteredLayout>
+ );
}
export default withComponentContext(withAvailableFeatures(ProjectDumpApp));
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Button } from '@sonarsource/echoes-react';
-import { FlagMessage, Link, Spinner } from 'design-system';
+import { Button, Spinner } from '@sonarsource/echoes-react';
+import { FlagMessage, Link } from 'design-system';
+import { noop } from 'lodash';
import * as React from 'react';
-import { doExport } from '../../../api/project-dump';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { DumpStatus, DumpTask } from '../../../types/project-dump';
+import { useLastActivityQuery } from '../../../queries/ce';
+import { useProjectDumpStatusQuery, useProjectExportMutation } from '../../../queries/project-dump';
+import { DumpTask } from '../../../types/project-dump';
+import { TaskStatuses, TaskTypes } from '../../../types/tasks';
+import { getImportExportActivityParams } from '../utils';
interface Props {
componentKey: string;
- loadStatus: () => void;
- status: DumpStatus;
- task?: DumpTask;
}
+const PROGRESS_STATUS = [TaskStatuses.Pending, TaskStatuses.InProgress];
+const REFRESH_INTERVAL = 5000;
+
+export default function Export({ componentKey }: Readonly<Props>) {
+ const { data: task, refetch: refetchLastActivity } = useLastActivityQuery(
+ getImportExportActivityParams(componentKey, TaskTypes.ProjectExport),
+ {
+ refetchInterval: ({ state: { data } }) => {
+ return data && PROGRESS_STATUS.includes(data.status) ? REFRESH_INTERVAL : undefined;
+ },
+ },
+ );
+ const { data: status, refetch: refetchDumpStatus } = useProjectDumpStatusQuery(componentKey, {
+ refetchInterval: () => {
+ return task && PROGRESS_STATUS.includes(task.status) ? REFRESH_INTERVAL : undefined;
+ },
+ });
+ const { mutateAsync: doExport } = useProjectExportMutation();
+
+ const isDumpAvailable = Boolean(status?.exportedDump);
-export default function Export(props: Readonly<Props>) {
const handleExport = async () => {
try {
- await doExport(props.componentKey);
- props.loadStatus();
- } catch (error) {
- /* no catch needed */
+ await doExport(componentKey);
+ refetchLastActivity();
+ refetchDumpStatus();
+ } catch (_) {
+ noop();
}
};
- function renderHeader() {
- return (
- <div className="sw-mb-4">
- <span className="sw-heading-md">{translate('project_dump.export')}</span>
- </div>
- );
- }
-
function renderWhenCanNotExport() {
return (
- <>
- {renderHeader()}
- <FlagMessage className="sw-mb-4" variant="warning">
- {translate('project_dump.can_not_export')}
- </FlagMessage>
- </>
+ <FlagMessage className="sw-mb-4" variant="warning">
+ {translate('project_dump.can_not_export')}
+ </FlagMessage>
);
}
function renderWhenExportPending(task: DumpTask) {
return (
- <>
- {renderHeader()}
- <div>
- <Spinner />
- <DateTimeFormatter date={task.submittedAt}>
- {(formatted) => (
- <span>{translateWithParameters('project_dump.pending_export', formatted)}</span>
- )}
- </DateTimeFormatter>
- </div>
- </>
+ <div className="sw-flex sw-gap-2">
+ <Spinner />
+ <DateTimeFormatter date={task.submittedAt}>
+ {(formatted) => (
+ <output>{translateWithParameters('project_dump.pending_export', formatted)}</output>
+ )}
+ </DateTimeFormatter>
+ </div>
);
}
function renderWhenExportInProgress(task: DumpTask) {
return (
- <>
- {renderHeader()}
- <div>
- <Spinner />
- {task.startedAt && (
- <DateFromNow date={task.startedAt}>
- {(fromNow) => (
- <span>{translateWithParameters('project_dump.in_progress_export', fromNow)}</span>
- )}
- </DateFromNow>
- )}
- </div>
- </>
+ <div className="sw-flex sw-gap-2">
+ <Spinner />
+ {task.startedAt && (
+ <DateFromNow date={task.startedAt}>
+ {(fromNow) => (
+ <output>{translateWithParameters('project_dump.in_progress_export', fromNow)}</output>
+ )}
+ </DateFromNow>
+ )}
+ </div>
);
}
function renderWhenExportFailed() {
- const { componentKey } = props;
const detailsUrl = `/project/background_tasks?id=${encodeURIComponent(
componentKey,
)}&status=FAILED&taskType=PROJECT_EXPORT`;
return (
- <>
- {renderHeader()}
- <div>
- <FlagMessage className="sw-mb-4" variant="error">
- {translate('project_dump.failed_export')}
- <Link className="sw-ml-1" to={detailsUrl}>
- {translate('project_dump.see_details')}
- </Link>
- </FlagMessage>
-
- {renderExport()}
- </div>
- </>
+ <div>
+ <FlagMessage className="sw-mb-4" variant="error">
+ {translate('project_dump.failed_export')}
+ <Link className="sw-ml-1" to={detailsUrl}>
+ {translate('project_dump.see_details')}
+ </Link>
+ </FlagMessage>
+
+ {renderExport()}
+ </div>
);
}
- function renderDump(task?: DumpTask) {
- const { status } = props;
-
+ function renderDump(task?: DumpTask | null) {
return (
<FlagMessage className="sw-mb-4" variant="success">
<div>
- {task && task.executedAt && (
+ {task?.executedAt && (
<DateTimeFormatter date={task.executedAt}>
{(formatted) => (
<div>
<div>
{!task && <div>{translate('project_dump.export_available')}</div>}
- <p className="sw-mt-2">{status.exportedDump}</p>
+ <p className="sw-mt-2">{status?.exportedDump}</p>
</div>
</div>
</FlagMessage>
);
}
- const { task, status } = props;
+ if (status === undefined || task === undefined) {
+ return <Spinner />;
+ }
if (!status.canBeExported) {
return renderWhenCanNotExport();
}
- if (task && task.status === 'PENDING') {
+ if (task?.status === TaskStatuses.Pending) {
return renderWhenExportPending(task);
}
- if (task && task.status === 'IN_PROGRESS') {
+ if (task?.status === TaskStatuses.InProgress) {
return renderWhenExportInProgress(task);
}
- if (task && task.status === 'FAILED') {
+ if (task?.status === TaskStatuses.Failed) {
return renderWhenExportFailed();
}
- const isDumpAvailable = Boolean(status.exportedDump);
-
return (
- <>
- {renderHeader()}
- <div>
- {isDumpAvailable && renderDump(task)}
- {renderExport()}
- </div>
- </>
+ <div>
+ {isDumpAvailable && renderDump(task)}
+ {renderExport()}
+ </div>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Button } from '@sonarsource/echoes-react';
-import { FlagMessage, Link, Spinner } from 'design-system';
+import { Button, Spinner } from '@sonarsource/echoes-react';
+import { FlagMessage, Link } from 'design-system';
+import { noop } from 'lodash';
import * as React from 'react';
-import { doImport } from '../../../api/project-dump';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getComponentBackgroundTaskUrl } from '../../../helpers/urls';
-import { DumpStatus, DumpTask } from '../../../types/project-dump';
+import { useLastActivityQuery } from '../../../queries/ce';
+import { StaleTime } from '../../../queries/common';
+import { useProjectDumpStatusQuery, useProjectImportMutation } from '../../../queries/project-dump';
+import { DumpTask } from '../../../types/project-dump';
import { TaskStatuses, TaskTypes } from '../../../types/tasks';
+import { getImportExportActivityParams } from '../utils';
interface Props {
- analysis?: DumpTask;
componentKey: string;
importEnabled: boolean;
- loadStatus: () => void;
- status: DumpStatus;
- task?: DumpTask;
}
+const PROGRESS_STATUS = [TaskStatuses.Pending, TaskStatuses.InProgress];
+const REFRESH_INTERVAL = 5000;
export default function Import(props: Readonly<Props>) {
- const handleImport = () => {
- doImport(props.componentKey).then(props.loadStatus, () => {
- /* no catch needed */
- });
+ const { componentKey, importEnabled } = props;
+
+ const {
+ data: task,
+ refetch: refetchImportActivity,
+ isLoading: isTaskLoading,
+ } = useLastActivityQuery(getImportExportActivityParams(componentKey, TaskTypes.ProjectImport), {
+ refetchInterval: ({ state: { data } }) => {
+ return data && PROGRESS_STATUS.includes(data.status) ? REFRESH_INTERVAL : undefined;
+ },
+ });
+ const { data: analysis, isLoading: isAnalysisActivityLoading } = useLastActivityQuery(
+ getImportExportActivityParams(componentKey, TaskTypes.Report),
+ {
+ staleTime: StaleTime.SHORT,
+ },
+ );
+ const {
+ data: status,
+ isLoading: isStatusLoading,
+ refetch: refetchDumpStatus,
+ } = useProjectDumpStatusQuery(componentKey, {
+ refetchInterval: () => {
+ return task && PROGRESS_STATUS.includes(task.status) ? REFRESH_INTERVAL : undefined;
+ },
+ });
+ const { mutateAsync: doImport } = useProjectImportMutation();
+ const isLoading = isTaskLoading || isAnalysisActivityLoading || isStatusLoading;
+
+ const handleImport = async () => {
+ try {
+ await doImport(componentKey);
+ refetchImportActivity();
+ refetchDumpStatus();
+ } catch (_) {
+ noop();
+ }
};
function renderWhenCanNotImport() {
);
}
- const { importEnabled, status, task, analysis } = props;
-
function renderContent(): React.ReactNode {
switch (task?.status) {
case TaskStatuses.Success:
case TaskStatuses.Failed:
return renderWhenImportFailed();
default:
- if (!status.canBeImported) {
+ if (status && !status.canBeImported) {
return renderWhenCanNotImport();
- } else if (!status.dumpToImport) {
+ } else if (status && status.dumpToImport === undefined) {
return renderWhenNoDump();
}
return <div>{renderImportForm()}</div>;
return (
<>
<div className="sw-my-4">
- <span className="sw-heading-md">{translate('project_dump.import')}</span>
+ <h2 className="sw-heading-md">{translate('project_dump.import')}</h2>
</div>
- {importEnabled ? (
- renderContent()
- ) : (
- <div>{translate('project_dump.import_form_description_disabled')}</div>
- )}
+ <Spinner isLoading={isLoading}>
+ {importEnabled ? (
+ renderContent()
+ ) : (
+ <div>{translate('project_dump.import_form_description_disabled')}</div>
+ )}
+ </Spinner>
</>
);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { TaskStatuses, TaskTypes } from '../../types/tasks';
+
+export const getImportExportActivityParams = (componentKey: string, taskType: TaskTypes) => {
+ return {
+ component: componentKey,
+ type: taskType,
+ ps: 1,
+ status: [
+ TaskStatuses.Pending,
+ TaskStatuses.InProgress,
+ TaskStatuses.Success,
+ TaskStatuses.Failed,
+ TaskStatuses.Canceled,
+ ].join(','),
+ };
+};
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { queryOptions } from '@tanstack/react-query';
+import { getActivity } from '../api/ce';
+import { ActivityRequestParameters } from '../types/tasks';
+import { createQueryHook } from './common';
+
+export const useLastActivityQuery = createQueryHook((data: ActivityRequestParameters) => {
+ return queryOptions({
+ queryKey: ['ce', 'activity', data.component, data.type, data.status],
+ queryFn: () => getActivity(data).then(({ tasks }) => (tasks.length > 0 ? tasks[0] : null)),
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { queryOptions, useMutation } from '@tanstack/react-query';
+import { doExport, doImport, getStatus } from '../api/project-dump';
+import { createQueryHook } from './common';
+
+export const useProjectDumpStatusQuery = createQueryHook((componentKey: string) => {
+ return queryOptions({
+ queryKey: ['project-dump', componentKey],
+ queryFn: () => getStatus(componentKey),
+ });
+});
+
+export const useProjectExportMutation = () =>
+ useMutation({
+ mutationFn: (componentKey: string) => doExport(componentKey),
+ });
+
+export const useProjectImportMutation = () =>
+ useMutation({
+ mutationFn: (componentKey: string) => doImport(componentKey),
+ });