]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22313 Fix A11y issues on import/export page (#11650)
authorSarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com>
Mon, 2 Sep 2024 17:54:41 +0000 (19:54 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 2 Sep 2024 20:02:50 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx
server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx
server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx
server/sonar-web/src/main/js/apps/projectDump/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/ce.ts [new file with mode: 0644]
server/sonar-web/src/main/js/queries/project-dump.ts [new file with mode: 0644]

index 5b30997db411e5b50a79ee45c1ae9ea29be82723..7c000544a0f9222ddaa8fff1b54d1931a96fbff6 100644 (file)
  * 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));
index 9b0175fc0e55f3a8a1667e25c0bd19d72e4ae378..2431737c7e20d9e19705fcf70897192d2530acae 100644 (file)
  * 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>
@@ -137,7 +135,7 @@ export default function Export(props: Readonly<Props>) {
           <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>
@@ -159,33 +157,30 @@ export default function Export(props: Readonly<Props>) {
     );
   }
 
-  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>
   );
 }
index 09126a9c59ba8b866447ea13930c62cac3f1745b..165815cefc3ddc2ea3015de0fbbe8b6eef608b0d 100644 (file)
  * 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() {
@@ -135,8 +170,6 @@ export default function Import(props: Readonly<Props>) {
     );
   }
 
-  const { importEnabled, status, task, analysis } = props;
-
   function renderContent(): React.ReactNode {
     switch (task?.status) {
       case TaskStatuses.Success:
@@ -151,9 +184,9 @@ export default function Import(props: Readonly<Props>) {
       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>;
@@ -163,14 +196,16 @@ export default function Import(props: Readonly<Props>) {
   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>
     </>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/projectDump/utils.ts b/server/sonar-web/src/main/js/apps/projectDump/utils.ts
new file mode 100644 (file)
index 0000000..73e8545
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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(','),
+  };
+};
diff --git a/server/sonar-web/src/main/js/queries/ce.ts b/server/sonar-web/src/main/js/queries/ce.ts
new file mode 100644 (file)
index 0000000..8cc36db
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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)),
+  });
+});
diff --git a/server/sonar-web/src/main/js/queries/project-dump.ts b/server/sonar-web/src/main/js/queries/project-dump.ts
new file mode 100644 (file)
index 0000000..5f5d7ff
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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),
+  });