Przeglądaj źródła

[ui-core] auto set qsEncodeData key in useSaveData hook (#4082)

Ram Prasad Agarwal 7 miesięcy temu
rodzic
commit
a0a6cd2376
20 zmienionych plików z 197 dodań i 47 usunięć
  1. 1 8
      desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx
  2. 1 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx
  3. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.tsx
  4. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx
  5. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx
  6. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/DeletionModal/DeletionModal.tsx
  7. 1 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx
  8. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.tsx
  9. 1 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx
  10. 1 0
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx
  11. 0 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx
  12. 3 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx
  13. 0 3
      desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx
  14. 1 3
      desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.tsx
  15. 1 6
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts
  16. 1 8
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.ts
  17. 93 0
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.test.tsx
  18. 2 0
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts
  19. 64 0
      desktop/core/src/desktop/js/utils/jsonUtils.test.ts
  20. 27 0
      desktop/core/src/desktop/js/utils/jsonUtils.ts

+ 1 - 8
desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/ImporterSourceSelector.tsx

@@ -30,14 +30,7 @@ const ImporterSourceSelector = (): JSX.Element => {
   const { t } = i18nReact.useTranslation();
   const uploadRef = useRef<HTMLInputElement>(null);
 
-  const { save: upload } = useSaveData(UPLOAD_LOCAL_FILE_API_URL, {
-    postOptions: {
-      qsEncodeData: false,
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
-    }
-  });
+  const { save: upload } = useSaveData(UPLOAD_LOCAL_FILE_API_URL);
 
   const handleUploadClick = () => {
     if (!uploadRef || !uploadRef.current) {

+ 1 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx

@@ -72,6 +72,7 @@ const CreateAndUploadAction = ({
   };
 
   const { save, loading } = useSaveData(undefined, {
+    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     onSuccess: onApiSuccess,
     onError: onActionError
   });

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal.tsx

@@ -67,9 +67,6 @@ const ChangeOwnerAndGroupModal = ({
   const [isRecursive, setIsRecursive] = useState<boolean>(false);
 
   const { save, loading } = useSaveData(BULK_CHANGE_OWNER_API_URL, {
-    postOptions: {
-      qsEncodeData: false
-    },
     skip: !files.length,
     onSuccess,
     onError

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ChangePermissionModal/ChangePermissionModal.tsx

@@ -51,9 +51,6 @@ const ChangePermissionModal = ({
   const [permissions, setPermissions] = useState<Permission[]>(initialPermissions);
 
   const { save, loading } = useSaveData(BULK_CHANGE_PERMISSION_API_URL, {
-    postOptions: {
-      qsEncodeData: false
-    },
     skip: !files.length,
     onSuccess,
     onError

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/CompressionModal/CompressionModal.tsx

@@ -48,9 +48,6 @@ const CompressionModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save: saveForm, loading } = useSaveData(COMPRESS_API_URL, {
-    postOptions: {
-      qsEncodeData: false
-    },
     skip: !files.length,
     onSuccess,
     onError

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/DeletionModal/DeletionModal.tsx

@@ -43,9 +43,6 @@ const DeletionModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading } = useSaveData(undefined, {
-    postOptions: {
-      qsEncodeData: false
-    },
     skip: !files.length,
     onSuccess,
     onError

+ 1 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx

@@ -43,6 +43,7 @@ const ExtractionModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading } = useSaveData(EXTRACT_API_URL, {
+    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file,
     onSuccess,
     onError

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.tsx

@@ -46,9 +46,6 @@ const MoveCopyModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save } = useSaveData(undefined, {
-    postOptions: {
-      qsEncodeData: false
-    },
     skip: !files.length,
     onSuccess: onSuccess,
     onError: onError

+ 1 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx

@@ -39,6 +39,7 @@ const RenameModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading } = useSaveData(RENAME_API_URL, {
+    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
     onError

+ 1 - 0
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx

@@ -39,6 +39,7 @@ const ReplicationModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading } = useSaveData(SET_REPLICATION_API_URL, {
+    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
     onError

+ 0 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx

@@ -51,9 +51,6 @@ const TrashActions = ({
   const [selectedAction, setSelectedAction] = useState<Actions | undefined>();
 
   const { save, loading } = useSaveData(undefined, {
-    postOptions: {
-      qsEncodeData: false
-    },
     onSuccess: () => {
       setIsModalOpen(false);
     },

+ 3 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx

@@ -53,7 +53,9 @@ const StorageFilePage = ({ fileStats, onReload }: StorageFilePageProps): JSX.Ele
   const pageSize = DEFAULT_PREVIEW_PAGE_SIZE;
   const pageOffset = (pageNumber - 1) * pageSize;
 
-  const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL);
+  const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL, {
+    postOptions: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload
+  });
 
   const { data, loading, error } = useLoadData<FilePreview>(FILE_PREVIEW_API_URL, {
     params: {

+ 0 - 3
desktop/core/src/desktop/js/reactComponents/TaskServer/ScheduleTaskModal/ScheduleTaskModal.tsx

@@ -47,9 +47,6 @@ const ScheduleTaskModal = ({ onClose, open = true }: ScheduleTaskModalProps): JS
   };
 
   const { save, loading, error } = useSaveData(SCHEDULE_NEW_TASK_URL, {
-    postOptions: {
-      qsEncodeData: false
-    },
     onSuccess: () => {
       huePubSub.publish('hue.global.info', { message: `Task submitted successfully` });
       onClose();

+ 1 - 3
desktop/core/src/desktop/js/reactComponents/TaskServer/TaskServer.tsx

@@ -50,9 +50,7 @@ export const TaskServer: React.FC = () => {
 
   const { t } = i18nReact.useTranslation();
 
-  const { save } = useSaveData(KILL_TASK_URL, {
-    postOptions: { qsEncodeData: false }
-  });
+  const { save } = useSaveData(KILL_TASK_URL);
 
   const columns = [
     {

+ 1 - 6
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts

@@ -66,12 +66,7 @@ const useChunkUpload = ({
   const [filesWaitingFinalStatus, setFilesWaitingFinalStatus] = useState<ChunkedFile['uuid'][]>([]);
   const [filesInProgress, setFilesInProgress] = useState<ChunkedFilesInProgress>({});
 
-  const { save } = useSaveData(undefined, {
-    postOptions: {
-      qsEncodeData: false,
-      headers: { 'Content-Type': 'multipart/form-data' }
-    }
-  });
+  const { save } = useSaveData();
 
   const processTaskServerResponse = (response: TaskServerResponse[]) => {
     const statusMap = getStatusHashMap(response);

+ 1 - 8
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.ts

@@ -38,14 +38,7 @@ const useRegularUpload = ({
   updateFileVariables,
   onComplete
 }: UploadQueueOptions): UseUploadQueueResponse => {
-  const { save } = useSaveData(UPLOAD_FILE_URL, {
-    postOptions: {
-      qsEncodeData: false,
-      headers: {
-        'Content-Type': 'multipart/form-data'
-      }
-    }
-  });
+  const { save } = useSaveData(UPLOAD_FILE_URL);
 
   const processRegularFile = async (item: RegularFile) => {
     updateFileVariables(item.uuid, { status: FileStatus.Uploading });

+ 93 - 0
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.test.tsx

@@ -13,6 +13,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 import { renderHook, act, waitFor } from '@testing-library/react';
 import useSaveData from './useSaveData';
 import { post } from '../../../api/utils';
@@ -51,6 +52,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(mockBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -75,6 +77,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(mockBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -110,6 +113,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(mockBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -128,6 +132,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(newBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -155,6 +160,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(mockBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -187,6 +193,7 @@ describe('useSaveData', () => {
     act(() => {
       result.current.save(mockBody);
     });
+
     expect(result.current.loading).toBe(true);
 
     await waitFor(() => {
@@ -198,4 +205,90 @@ describe('useSaveData', () => {
       expect(mockOnError).toHaveBeenCalledWith(mockError);
     });
   });
+
+  it('should auto set qsEncodeData to true when body is not a FormData or JSON', async () => {
+    const { result } = renderHook(() => useSaveData(mockUrl));
+
+    act(() => {
+      result.current.save('hue data');
+    });
+
+    await waitFor(() => {
+      expect(mockPost).toHaveBeenCalledWith(
+        mockUrl,
+        'hue data',
+        expect.objectContaining({ qsEncodeData: true })
+      );
+      expect(result.current.data).toEqual(mockData);
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(false);
+    });
+  });
+
+  it('should not auto set qsEncodeData to true when body is a FormData', async () => {
+    const payload = new FormData();
+
+    const { result } = renderHook(() => useSaveData(mockUrl));
+
+    act(() => {
+      result.current.save(payload);
+    });
+
+    await waitFor(() => {
+      expect(mockPost).toHaveBeenCalledWith(
+        mockUrl,
+        payload,
+        expect.objectContaining({ qsEncodeData: false })
+      );
+      expect(result.current.data).toEqual(mockData);
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(false);
+    });
+  });
+
+  it('should not auto set qsEncodeData to true when body is a JSON', async () => {
+    const payload = { project: 'hue' };
+
+    const { result } = renderHook(() => useSaveData(mockUrl));
+
+    act(() => {
+      result.current.save(payload);
+    });
+
+    await waitFor(() => {
+      expect(mockPost).toHaveBeenCalledWith(
+        mockUrl,
+        payload,
+        expect.objectContaining({ qsEncodeData: false })
+      );
+      expect(result.current.data).toEqual(mockData);
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(false);
+    });
+  });
+
+  it('should take qsEncodeData value from saveOptions.postOptions when available', async () => {
+    const payload = new FormData();
+
+    const { result } = renderHook(() =>
+      useSaveData(mockUrl, {
+        postOptions: { qsEncodeData: true }
+      })
+    );
+
+    act(() => {
+      result.current.save(payload);
+    });
+
+    await waitFor(() => {
+      expect(mockPost).toHaveBeenCalledWith(
+        mockUrl,
+        payload,
+        expect.objectContaining({ qsEncodeData: true })
+      );
+      expect(result.current.data).toEqual(mockData);
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(false);
+    });
+  });
 });

+ 2 - 0
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

@@ -16,6 +16,7 @@
 
 import { useCallback, useEffect, useState } from 'react';
 import { ApiFetchOptions, post } from '../../../api/utils';
+import { isJSON } from '../../jsonUtils';
 
 interface saveOptions<T> {
   url?: string;
@@ -59,6 +60,7 @@ const useSaveData = <T, U = unknown>(url?: string, options?: Options<T>): UseSav
 
       const postOptions = {
         ...postOptionsDefault,
+        qsEncodeData: body instanceof FormData || isJSON(body) ? false : true,
         ...localOptions?.postOptions,
         ...saveOptions?.postOptions
       };

+ 64 - 0
desktop/core/src/desktop/js/utils/jsonUtils.test.ts

@@ -0,0 +1,64 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { isJSON } from './jsonUtils';
+
+describe('isJSON function', () => {
+  it('should return false if data is null', () => {
+    expect(isJSON(null)).toBe(false);
+  });
+
+  it('should return false if data is undefined', () => {
+    expect(isJSON(undefined)).toBe(false);
+  });
+
+  it('should return true if data is a plain object', () => {
+    const obj = { key: 'value' };
+    expect(isJSON(obj)).toBe(true);
+  });
+
+  it('should return false if data is an instance of FormData', () => {
+    const formData = new FormData();
+    expect(isJSON(formData)).toBe(false);
+  });
+
+  it('should return false if data is an instance of Blob', () => {
+    const blob = new Blob(['test'], { type: 'text/plain' });
+    expect(isJSON(blob)).toBe(false);
+  });
+
+  it('should return false if data is an instance of ArrayBuffer', () => {
+    const arrayBuffer = new ArrayBuffer(8);
+    expect(isJSON(arrayBuffer)).toBe(false);
+  });
+
+  it('should return false if data is an array', () => {
+    const arr = [1, 2, 3];
+    expect(isJSON(arr)).toBe(false);
+  });
+
+  it('should return false if data is a number', () => {
+    expect(isJSON(123)).toBe(false);
+  });
+
+  it('should return false if data is a string', () => {
+    expect(isJSON('string')).toBe(false);
+  });
+
+  it('should return false if data is a boolean', () => {
+    expect(isJSON(true)).toBe(false);
+  });
+});

+ 27 - 0
desktop/core/src/desktop/js/utils/jsonUtils.ts

@@ -0,0 +1,27 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export const isJSON = (data: unknown): boolean => {
+  return (
+    data !== null &&
+    data !== undefined &&
+    typeof data === 'object' &&
+    !(data instanceof FormData) &&
+    !(data instanceof Blob) &&
+    !(data instanceof Array) &&
+    !(data instanceof ArrayBuffer)
+  );
+};