Просмотр исходного кода

[ui-storagebrowser] converts snakecase keys to camel in useLoadData hook (#4011)

Ram Prasad Agarwal 8 месяцев назад
Родитель
Сommit
14581860d0
18 измененных файлов с 520 добавлено и 126 удалено
  1. 1 1
      apps/filebrowser/src/filebrowser/api.py
  2. 5 5
      apps/filebrowser/src/filebrowser/api_test.py
  3. 2 2
      desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.test.tsx
  4. 25 32
      desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx
  5. 5 5
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage.tsx
  6. 14 14
      desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx
  7. 12 4
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.tsx
  8. 8 8
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx
  9. 2 2
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx
  10. 5 5
      desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx
  11. 18 16
      desktop/core/src/desktop/js/apps/storageBrowser/types.ts
  12. 5 5
      desktop/core/src/desktop/js/reactComponents/FileChooser/FileChooserModal/FileChooserModal.tsx
  13. 14 14
      desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.tsx
  14. 2 1
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts
  15. 123 9
      desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.test.tsx
  16. 18 3
      desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.ts
  17. 208 0
      desktop/core/src/desktop/js/utils/string/changeCasing.test.ts
  18. 53 0
      desktop/core/src/desktop/js/utils/string/changeCasing.ts

+ 1 - 1
apps/filebrowser/src/filebrowser/api.py

@@ -165,7 +165,7 @@ def get_all_filesystems(request):
     user_home_dir = fs_home_dir_mapping[fs](request.user)
     config = _get_config(fs, request)
 
-    filesystems.append({'file_system': fs, 'user_home_directory': user_home_dir, 'config': config})
+    filesystems.append({'name': fs, 'user_home_directory': user_home_dir, 'config': config})
 
   return JsonResponse(filesystems, safe=False)
 

+ 5 - 5
apps/filebrowser/src/filebrowser/api_test.py

@@ -748,8 +748,8 @@ class TestGetFilesystemsAPI:
 
           assert response.status_code == 200
           assert response_data == [
-            {'file_system': 's3a', 'user_home_directory': 's3a://test-bucket/test-user-home-dir/', 'config': {}},
-            {'file_system': 'ofs', 'user_home_directory': 'ofs://', 'config': {}},
+            {'name': 's3a', 'user_home_directory': 's3a://test-bucket/test-user-home-dir/', 'config': {}},
+            {'name': 'ofs', 'user_home_directory': 'ofs://', 'config': {}},
           ]
 
   def test_get_all_filesystems_success(self):
@@ -776,7 +776,7 @@ class TestGetFilesystemsAPI:
               assert response.status_code == 200
               assert response_data == [
                 {
-                  'file_system': 'hdfs',
+                  'name': 'hdfs',
                   'user_home_directory': '/user/test-user',
                   'config': {
                     'is_trash_enabled': False,
@@ -787,8 +787,8 @@ class TestGetFilesystemsAPI:
                     'supergroup': 'test-supergroup',
                   },
                 },
-                {'file_system': 's3a', 'user_home_directory': 's3a://test-bucket/test-user-home-dir/', 'config': {}},
-                {'file_system': 'ofs', 'user_home_directory': 'ofs://', 'config': {}},
+                {'name': 's3a', 'user_home_directory': 's3a://test-bucket/test-user-home-dir/', 'config': {}},
+                {'name': 'ofs', 'user_home_directory': 'ofs://', 'config': {}},
               ]
 
 

+ 2 - 2
desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.test.tsx

@@ -24,12 +24,12 @@ import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 
 const mockData = jest.fn().mockReturnValue({
   logs: ['Log entry 1', 'Log entry 2'],
-  hue_hostname: 'test-hostname'
+  hueHostname: 'test-hostname'
 });
 
 const emptyMockData = jest.fn().mockReturnValue({
   logs: [],
-  hue_hostname: 'test-hostname'
+  hueHostname: 'test-hostname'
 });
 
 jest.mock('../../../utils/hooks/useLoadData/useLoadData');

+ 25 - 32
desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx

@@ -15,28 +15,25 @@
 // limitations under the License.
 
 import React, { useState } from 'react';
-import { Spin, Alert } from 'antd';
+import { Alert } from 'antd';
 import ServerLogsHeader from './ServerLogsHeader';
 import { i18nReact } from '../../../utils/i18nReact';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
+import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/LoadingErrorWrapper';
 import HighlightText from '../Components/HighlightText';
 import { SERVER_LOGS_API_URL } from '../Components/utils';
 import './ServerLogsTab.scss';
 
 interface ServerLogsData {
   logs: string[];
-  hue_hostname: string;
+  hueHostname: string;
 }
 
 const ServerLogs: React.FC = (): JSX.Element => {
   const [filter, setFilter] = useState<string>('');
   const [wrapLogs, setWrapLogs] = useState(true);
   const { t } = i18nReact.useTranslation();
-  const {
-    data: logsData,
-    loading,
-    error
-  } = useLoadData<ServerLogsData>(SERVER_LOGS_API_URL, {
+  const { data, loading, error } = useLoadData<ServerLogsData>(SERVER_LOGS_API_URL, {
     params: {
       reverse: true
     }
@@ -54,35 +51,31 @@ const ServerLogs: React.FC = (): JSX.Element => {
     );
   }
 
+  const isEmptyLogs = !data?.logs || !data?.logs?.some(log => log.length);
+
   return (
     <div className="hue-server-logs-component">
-      <Spin spinning={loading}>
-        {!loading && (
-          <>
-            <ServerLogsHeader
-              onFilterChange={setFilter}
-              onWrapLogsChange={setWrapLogs}
-              hostName={logsData?.hue_hostname ?? ''}
-            />
-            {logsData && (logsData.logs.length === 0 || logsData.logs[0] === '') && (
-              <pre className="server__no-logs-found">No logs found!</pre>
-            )}
-
-            {logsData && logsData.logs.length > 0 && logsData.logs[0] !== '' && (
-              <div className="server__display-logs">
-                {logsData.logs.map((line, index) => (
-                  <div
-                    className={`server__log-line ${wrapLogs ? 'server__log-line--wrap' : ''}`}
-                    key={'logs_' + index}
-                  >
-                    <HighlightText text={line} searchValue={filter} />
-                  </div>
-                ))}
+      <LoadingErrorWrapper loading={loading} errors={[]}>
+        <ServerLogsHeader
+          onFilterChange={setFilter}
+          onWrapLogsChange={setWrapLogs}
+          hostName={data?.hueHostname ?? ''}
+        />
+        {isEmptyLogs ? (
+          <pre className="server__no-logs-found">No logs found!</pre>
+        ) : (
+          <div className="server__display-logs">
+            {data.logs.map((line, index) => (
+              <div
+                className={`server__log-line ${wrapLogs ? 'server__log-line--wrap' : ''}`}
+                key={'logs_' + index}
+              >
+                <HighlightText text={line} searchValue={filter} />
               </div>
-            )}
-          </>
+            ))}
+          </div>
         )}
-      </Spin>
+      </LoadingErrorWrapper>
     </div>
   );
 };

+ 5 - 5
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage.tsx

@@ -55,11 +55,11 @@ const StorageBrowserPage = (): JSX.Element => {
         <Tabs
           className="hue-storage-browser__tab"
           destroyInactiveTabPane
-          defaultActiveKey={urlFileSystem ?? data?.[0]?.file_system}
-          items={data?.map(fs => ({
-            label: fs.file_system.toUpperCase(),
-            key: fs.file_system,
-            children: <StorageBrowserTab fileSystem={fs} />
+          defaultActiveKey={urlFileSystem ?? data?.[0]?.name}
+          items={data?.map(fileSystem => ({
+            label: fileSystem.name.toUpperCase(),
+            key: fileSystem.name,
+            children: <StorageBrowserTab fileSystem={fileSystem} />
           }))}
         />
       </LoadingErrorWrapper>

+ 14 - 14
desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx

@@ -22,7 +22,7 @@ import BucketIcon from '@cloudera/cuix-core/icons/react/BucketIcon';
 import PathBrowser from '../../../reactComponents/PathBrowser/PathBrowser';
 import StorageDirectoryPage from '../StorageDirectoryPage/StorageDirectoryPage';
 import { FILE_STATS_API_URL, TRASH_PATH } from '../api';
-import { BrowserViewType, FileStats, FileSystem, TrashPath } from '../types';
+import { BrowserViewType, FileStats, FileSystem, TrashData } from '../types';
 import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
 import { BorderlessButton } from 'cuix/dist/components/Button';
 
@@ -52,7 +52,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
   const urlFilePath = decodeURIComponent(urlSearchParams.get('path') ?? '');
   const { fileSystem: urlFileSystem } = getFileSystemAndPath(urlFilePath);
   const initialFilePath =
-    urlFileSystem === fileSystem.file_system ? urlFilePath : fileSystem.user_home_directory;
+    urlFileSystem === fileSystem.name ? urlFilePath : fileSystem.userHomeDirectory;
 
   const [filePath, setFilePath] = useState<string>(initialFilePath);
   const fileName =
@@ -61,21 +61,21 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
   const { t } = i18nReact.useTranslation();
 
   const {
-    data: trashPath,
+    data: trashData,
     loading: trashLoading,
     reloadData: onTrashPathReload
-  } = useLoadData<TrashPath>(TRASH_PATH, {
-    params: { path: fileSystem.user_home_directory },
-    skip: !fileSystem.config?.is_trash_enabled || !fileSystem.user_home_directory
+  } = useLoadData<TrashData>(TRASH_PATH, {
+    params: { path: fileSystem.userHomeDirectory },
+    skip: !fileSystem.config?.isTrashEnabled || !fileSystem.userHomeDirectory
   });
 
   const onTrashClick = async () => {
     const latestTrashData = await onTrashPathReload();
-    setFilePath(latestTrashData?.trash_path ?? '');
+    setFilePath(latestTrashData?.trashPath ?? '');
   };
 
   const reloadTrashPath = () => {
-    if (trashPath?.trash_path) {
+    if (trashData?.trashPath) {
       return;
     }
     onTrashPathReload();
@@ -108,12 +108,12 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
       enabled: error?.response?.status === 404,
       message: t('Error: Path "{{path}}" not found.', { path: filePath }),
       action: t('Go to home directory'),
-      onClick: () => setFilePath(fileSystem.user_home_directory)
+      onClick: () => setFilePath(fileSystem.userHomeDirectory)
     },
     {
       enabled: !!error && error?.response?.status !== 404,
       message: t('An error occurred while fetching filesystem "{{fileSystem}}".', {
-        fileSystem: fileSystem.file_system.toUpperCase()
+        fileSystem: fileSystem.name.toUpperCase()
       }),
       action: t('Retry'),
       onClick: reloadData
@@ -135,7 +135,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
           <div className="hue-storage-browser__home-bar-right">
             <BorderlessButton
               onClick={() => {
-                setFilePath(fileSystem.user_home_directory);
+                setFilePath(fileSystem.userHomeDirectory);
               }}
               className="hue-storage-browser__home-bar-btns"
               data-event=""
@@ -144,14 +144,14 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
             >
               {t('Home')}
             </BorderlessButton>
-            {fileSystem.config?.is_trash_enabled && (
+            {fileSystem.config?.isTrashEnabled && (
               <BorderlessButton
                 onClick={onTrashClick}
                 className="hue-path-browser__home-btn"
                 data-event=""
                 title={t('Trash')}
                 icon={<DeleteIcon />}
-                disabled={!trashPath?.trash_path}
+                disabled={!trashData?.trashPath}
               >
                 {t('Trash')}
               </BorderlessButton>
@@ -167,7 +167,7 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
             </BorderlessButton>
           </div>
         </div>
-        {!!inTrash(filePath) && fileSystem.file_system === 'hdfs' && (
+        {!!inTrash(filePath) && fileSystem.name === 'hdfs' && (
           <Alert
             type="warning"
             message={t(

+ 12 - 4
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/FileAndFolderActions.tsx

@@ -35,7 +35,7 @@ import ConfigureIcon from '@cloudera/cuix-core/icons/react/ConfigureIcon';
 import { i18nReact } from '../../../../../utils/i18nReact';
 import huePubSub from '../../../../../utils/huePubSub';
 import './FileAndFolderActions.scss';
-import { FileStats, FileSystem, StorageDirectoryTableData } from '../../../types';
+import { FileStats, HDFSFileSystemConfig, StorageDirectoryTableData } from '../../../types';
 import { ActionType, getEnabledActions } from './FileAndFolderActions.util';
 import MoveCopyModal from './MoveCopyModal/MoveCopyModal';
 import RenameModal from './RenameModal/RenameModal';
@@ -48,8 +48,16 @@ import { DOWNLOAD_API_URL } from '../../../api';
 import ChangeOwnerAndGroupModal from './ChangeOwnerAndGroupModal/ChangeOwnerAndGroupModal';
 import ChangePermissionModal from './ChangePermissionModal/ChangePermissionModal';
 
+interface ActionConfig {
+  isTrashEnabled: HDFSFileSystemConfig['isTrashEnabled'];
+  isHdfsSuperuser: HDFSFileSystemConfig['isHdfsSuperuser'];
+  groups: HDFSFileSystemConfig['groups'];
+  supergroup: HDFSFileSystemConfig['supergroup'];
+  users: HDFSFileSystemConfig['users'];
+  superuser: HDFSFileSystemConfig['superuser'];
+}
 interface StorageBrowserRowActionsProps {
-  config: FileSystem['config'];
+  config: ActionConfig;
   currentPath: FileStats['path'];
   selectedFiles: StorageDirectoryTableData[];
   onActionSuccess: () => void;
@@ -105,7 +113,7 @@ const FileAndFolderActions = ({
   };
 
   const actionItems: MenuItemType[] = useMemo(() => {
-    const enabledActions = getEnabledActions(t, selectedFiles, config?.is_hdfs_superuser);
+    const enabledActions = getEnabledActions(t, selectedFiles, config?.isHdfsSuperuser);
     return enabledActions.map(action => ({
       key: String(action.type),
       label: t(action.label),
@@ -163,7 +171,7 @@ const FileAndFolderActions = ({
         )}
       {selectedAction === ActionType.Delete && !!selectedFiles.length && (
         <DeletionModal
-          isTrashEnabled={config?.is_trash_enabled}
+          isTrashEnabled={config?.isTrashEnabled}
           files={selectedFiles}
           onSuccess={onApiSuccess}
           onError={onActionError}

+ 8 - 8
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/SummaryModal/SummaryModal.tsx

@@ -36,7 +36,7 @@ interface SummaryModalProps {
 const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const { data: responseSummary, loading } = useLoadData<ContentSummary>(CONTENT_SUMMARY_API_URL, {
+  const { data, loading } = useLoadData<ContentSummary>(CONTENT_SUMMARY_API_URL, {
     params: { path: path },
     onError: error => {
       huePubSub.publish('hue.error', error);
@@ -48,31 +48,31 @@ const SummaryModal = ({ isOpen = true, onClose, path }: SummaryModalProps): JSX.
     {
       key: 'diskspaceConsumed',
       label: t('Diskspace Consumed'),
-      value: formatBytes(responseSummary?.spaceConsumed)
+      value: formatBytes(data?.spaceConsumed)
     },
-    { key: 'bytesUsed', label: t('Bytes Used'), value: formatBytes(responseSummary?.length) },
+    { key: 'bytesUsed', label: t('Bytes Used'), value: formatBytes(data?.length) },
     {
       key: 'namespaceQuota',
       label: t('Namespace Quota'),
-      value: formatBytes(responseSummary?.quota)
+      value: formatBytes(data?.quota)
     },
     {
       key: 'diskspaceQuota',
       label: t('Diskspace Quota'),
-      value: formatBytes(responseSummary?.spaceQuota)
+      value: formatBytes(data?.spaceQuota)
     },
     {
       key: 'replicationFactor',
       label: t('Replication Factor'),
-      value: responseSummary?.replication
+      value: data?.replication
     },
     { key: 'blank', label: '', value: '' },
     {
       key: 'numberOfDirectories',
       label: t('Number of Directories'),
-      value: responseSummary?.directoryCount
+      value: data?.directoryCount
     },
-    { key: 'numberOfFiles', label: t('Number of Files'), value: responseSummary?.fileCount }
+    { key: 'numberOfFiles', label: t('Number of Files'), value: data?.fileCount }
   ];
 
   const shortendPath =

+ 2 - 2
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryPage.tsx

@@ -303,12 +303,12 @@ const StorageDirectoryPage = ({
                 ...rowSelection
               }}
               scroll={{ y: tableBodyHeight }}
-              data-testid={`${testId}`}
+              data-testid={testId}
               locale={locale}
               {...restProps}
             />
 
-            {filesData?.page && filesData?.page?.total_pages > 0 && (
+            {filesData?.page && filesData?.page?.totalPages > 0 && (
               <Pagination
                 setPageSize={setPageSize}
                 pageSize={pageSize}

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

@@ -118,10 +118,10 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
     !inTrash(fileStats.path);
 
   const pageStats = {
-    page_number: pageNumber,
-    total_pages: Math.ceil(fileStats.size / pageSize),
-    page_size: 0,
-    total_size: 0
+    pageNumber: pageNumber,
+    totalPages: Math.ceil(fileStats.size / pageSize),
+    pageSize: 0,
+    totalSize: 0
   };
 
   const errorConfig = [
@@ -208,7 +208,7 @@ const StorageFilePage = ({ fileName, fileStats, onReload }: StorageFilePageProps
                     readOnly={!isEditing}
                     className="preview__textarea"
                   />
-                  {!loadingPreview && pageStats.total_pages > 1 && (
+                  {pageStats.totalPages > 1 && (
                     <Pagination setPageNumber={setPageNumber} pageStats={pageStats} />
                   )}
                 </div>

+ 18 - 16
desktop/core/src/desktop/js/apps/storageBrowser/types.ts

@@ -14,17 +14,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+export interface HDFSFileSystemConfig {
+  isTrashEnabled: boolean;
+  isHdfsSuperuser: boolean;
+  groups: string[];
+  users: string[];
+  superuser: string;
+  supergroup: string;
+}
+
 export interface FileSystem {
-  file_system: string;
-  user_home_directory: string;
-  config?: {
-    is_trash_enabled: boolean;
-    is_hdfs_superuser: boolean;
-    groups: string[];
-    users: string[];
-    superuser: string;
-    supergroup: string;
-  };
+  name: string;
+  userHomeDirectory: string;
+  config: HDFSFileSystemConfig;
 }
 
 export interface FileStats {
@@ -50,10 +52,10 @@ export interface StorageDirectoryTableData
 }
 
 export interface PageStats {
-  page_number: number;
-  total_pages: number;
-  page_size: number;
-  total_size: number;
+  pageNumber: number;
+  totalPages: number;
+  pageSize: number;
+  totalSize: number;
 }
 
 export interface FilePreview {
@@ -81,8 +83,8 @@ export interface ContentSummary {
   replication: number;
 }
 
-export interface TrashPath {
-  trash_path: string;
+export interface TrashData {
+  trashPath: string;
 }
 
 export enum SortOrder {

+ 5 - 5
desktop/core/src/desktop/js/reactComponents/FileChooser/FileChooserModal/FileChooserModal.tsx

@@ -61,18 +61,18 @@ const FileChooserModal: React.FC<FileProps> = ({ show, onCancel, title, okText }
 
   const fileSystemList: FileChooserMenu[] | undefined = useMemo(
     () =>
-      data?.map((system, index) => ({
-        label: system.file_system,
+      data?.map((fileSystem, index) => ({
+        label: fileSystem.name,
         key: index,
-        icon: icons[system.file_system],
-        user_home_dir: system.user_home_directory
+        icon: icons[fileSystem.name],
+        user_home_dir: fileSystem.userHomeDirectory
       })),
     [data]
   );
 
   useEffect(() => {
     if (data && data?.length !== 0) {
-      setFilePath(data[0].user_home_directory);
+      setFilePath(data[0].userHomeDirectory);
     }
   }, [data]);
 

+ 14 - 14
desktop/core/src/desktop/js/reactComponents/Pagination/Pagination.tsx

@@ -24,7 +24,7 @@ import PageNextIcon from '@cloudera/cuix-core/icons/react/PageNextIcon';
 import PageLastIcon from '@cloudera/cuix-core/icons/react/PageLastIcon';
 import DropdownIcon from '@cloudera/cuix-core/icons/react/DropdownIcon';
 
-import { PageStats } from '../FileChooser/types';
+import { PageStats } from '../../apps/storageBrowser/types';
 import './Pagination.scss';
 
 interface PaginationProps {
@@ -49,8 +49,8 @@ const Pagination = ({
 }: PaginationProps): JSX.Element => {
   const { t } = i18nReact.useTranslation();
 
-  const startIndex = pageStats.page_size * (pageStats.page_number - 1) + 1;
-  const endIndex = Math.min(pageStats.page_size * pageStats.page_number, pageStats.total_size);
+  const startIndex = pageStats.pageSize * (pageStats.pageNumber - 1) + 1;
+  const endIndex = Math.min(pageStats.pageSize * pageStats.pageNumber, pageStats.totalSize);
 
   const pageSizeOptionsMenu: MenuProps['items'] = pageSizeOptions.map(option => {
     return {
@@ -72,7 +72,7 @@ const Pagination = ({
 
   return (
     <div className="hue-pagination">
-      {pageStats.page_size > 0 && (
+      {pageStats.pageSize > 0 && (
         <div className="hue-pagination__page-size-control">
           {t('Rows per page: ')}
           <Dropdown menu={{ items: pageSizeOptionsMenu }}>
@@ -82,46 +82,46 @@ const Pagination = ({
               icon={<DropdownIcon />}
               iconPosition="right"
             >
-              {pageStats.page_size}
+              {pageStats.pageSize}
             </BorderlessButton>
           </Dropdown>
         </div>
       )}
       <div className="hue-pagination__rows-stats-display">
         {showIndexes
-          ? `${startIndex} - ${endIndex} of ${pageStats.total_size}`
-          : `${pageStats.page_number} of ${pageStats.total_pages}`}
+          ? `${startIndex} - ${endIndex} of ${pageStats.totalSize}`
+          : `${pageStats.pageNumber} of ${pageStats.totalPages}`}
       </div>
       <div className="hue-pagination__control-buttons-panel">
         <BorderlessButton
           onClick={() => setPageNumber(1)}
           className="hue-pagination__control-button"
           data-event={''}
-          disabled={pageStats.page_number === 1}
+          disabled={pageStats.pageNumber === 1}
           title={t('First Page')}
           icon={<PageFirstIcon />}
         />
         <BorderlessButton
-          onClick={() => setPageNumber(pageStats.page_number - 1)}
+          onClick={() => setPageNumber(pageStats.pageNumber - 1)}
           className="hue-pagination__control-button"
           data-event={''}
-          disabled={pageStats.page_number === 1}
+          disabled={pageStats.pageNumber === 1}
           title={t('First Page')}
           icon={<PagePreviousIcon />}
         />
         <BorderlessButton
-          onClick={() => setPageNumber(pageStats.page_number + 1)}
+          onClick={() => setPageNumber(pageStats.pageNumber + 1)}
           className="hue-pagination__control-button"
           data-event={''}
-          disabled={pageStats.page_number === pageStats.total_pages}
+          disabled={pageStats.pageNumber === pageStats.totalPages}
           title={t('Next Page')}
           icon={<PageNextIcon />}
         />
         <BorderlessButton
-          onClick={() => setPageNumber(pageStats.total_pages)}
+          onClick={() => setPageNumber(pageStats.totalPages)}
           className="hue-pagination__control-button"
           data-event={''}
-          disabled={pageStats.page_number === pageStats.total_pages}
+          disabled={pageStats.pageNumber === pageStats.totalPages}
           title={t('Last Page')}
           icon={<PageLastIcon />}
         />

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

@@ -104,7 +104,8 @@ const useChunkUpload = ({
     '/desktop/api2/taskserver/get_taskserver_tasks/',
     {
       pollInterval: awaitingStatusItems.length ? 5000 : undefined,
-      skip: !awaitingStatusItems.length
+      skip: !awaitingStatusItems.length,
+      transformKeys: 'none'
     }
   );
 

+ 123 - 9
desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.test.tsx

@@ -17,6 +17,7 @@
 import { renderHook, waitFor } from '@testing-library/react';
 import useLoadData from './useLoadData';
 import { get } from '../../../api/utils';
+import { convertKeysToCamelCase } from '../../string/changeCasing';
 
 // Mock the `get` function
 jest.mock('../../../api/utils', () => ({
@@ -27,7 +28,8 @@ const mockGet = get as jest.MockedFunction<typeof get>;
 const mockUrlPrefix = 'https://api.example.com';
 const mockEndpoint = '/endpoint';
 const mockUrl = `${mockUrlPrefix}${mockEndpoint}`;
-const mockData = { id: 1, product: 'Hue' };
+const mockData = { product_id: 1, product_name: 'Hue' };
+const mockDataResponse = convertKeysToCamelCase(mockData);
 const mockOptions = {
   params: { id: 1 }
 };
@@ -53,7 +55,7 @@ describe('useLoadData', () => {
 
     await waitFor(() => {
       expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
-      expect(result.current.data).toEqual(mockData);
+      expect(result.current.data).toEqual(mockDataResponse);
       expect(result.current.error).toBeUndefined();
       expect(result.current.loading).toBe(false);
     });
@@ -68,7 +70,7 @@ describe('useLoadData', () => {
 
     await waitFor(() => {
       expect(mockGet).toHaveBeenCalledWith(mockUrl, mockOptions.params, expect.any(Object));
-      expect(result.current.data).toEqual(mockData);
+      expect(result.current.data).toEqual(mockDataResponse);
       expect(result.current.error).toBeUndefined();
       expect(result.current.loading).toBe(false);
     });
@@ -110,12 +112,12 @@ describe('useLoadData', () => {
 
     await waitFor(() => {
       expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
-      expect(result.current.data).toEqual(mockData);
+      expect(result.current.data).toEqual(mockDataResponse);
       expect(result.current.error).toBeUndefined();
       expect(result.current.loading).toBe(false);
     });
 
-    const updatedMockResult = { ...mockData, product: 'Hue 2' };
+    const updatedMockResult = { ...mockDataResponse, product: 'Hue 2' };
     mockGet.mockResolvedValueOnce(updatedMockResult);
 
     const reloadResult = await result.current.reloadData();
@@ -144,7 +146,7 @@ describe('useLoadData', () => {
 
     await waitFor(() => {
       expect(mockGet).toHaveBeenCalledWith(mockUrl, mockOptions.params, expect.any(Object));
-      expect(result.current.data).toEqual(mockData);
+      expect(result.current.data).toEqual(mockDataResponse);
       expect(result.current.error).toBeUndefined();
       expect(result.current.loading).toBe(false);
     });
@@ -152,7 +154,7 @@ describe('useLoadData', () => {
     const newOptions = {
       params: { id: 2 }
     };
-    const newMockData = { ...mockData, id: 2 };
+    const newMockData = { ...mockDataResponse, productId: 2 };
     mockGet.mockResolvedValueOnce(newMockData);
 
     rerender({ url: mockUrl, options: newOptions });
@@ -181,10 +183,10 @@ describe('useLoadData', () => {
 
     await waitFor(() => {
       expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
-      expect(result.current.data).toEqual(mockData);
+      expect(result.current.data).toEqual(mockDataResponse);
       expect(result.current.error).toBeUndefined();
       expect(result.current.loading).toBe(false);
-      expect(mockOnSuccess).toHaveBeenCalledWith(mockData);
+      expect(mockOnSuccess).toHaveBeenCalledWith(mockDataResponse);
       expect(mockOnError).not.toHaveBeenCalled();
     });
   });
@@ -215,4 +217,116 @@ describe('useLoadData', () => {
       expect(mockOnError).toHaveBeenCalledWith(mockError);
     });
   });
+
+  describe('transformKeys option', () => {
+    it('should fetch data and transform keys to camelCase when transformKeys is "camelCase"', async () => {
+      const { result } = renderHook(() =>
+        useLoadData(mockUrl, {
+          transformKeys: 'camelCase'
+        })
+      );
+
+      expect(result.current.data).toBeUndefined();
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(() => {
+        expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
+        expect(result.current.data).toEqual({ productId: 1, productName: 'Hue' });
+        expect(result.current.error).toBeUndefined();
+        expect(result.current.loading).toBe(false);
+      });
+    });
+
+    it('should fetch data without transforming keys when transformKeys is "none"', async () => {
+      const { result } = renderHook(() =>
+        useLoadData(mockUrl, {
+          transformKeys: 'none'
+        })
+      );
+
+      expect(result.current.data).toBeUndefined();
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(() => {
+        expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
+        expect(result.current.data).toEqual(mockData);
+        expect(result.current.error).toBeUndefined();
+        expect(result.current.loading).toBe(false);
+      });
+    });
+
+    it('should fetch data and transform keys to camelCase when transformKeys is undefined', async () => {
+      const { result } = renderHook(() => useLoadData(mockUrl));
+
+      expect(result.current.data).toBeUndefined();
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(() => {
+        expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
+        expect(result.current.data).toEqual({ productId: 1, productName: 'Hue' });
+        expect(result.current.error).toBeUndefined();
+        expect(result.current.loading).toBe(false);
+      });
+    });
+
+    it('should handle camelCase transformation for nested objects', async () => {
+      const mockData = {
+        product_details: {
+          product_id: 1,
+          product_name: 'Hue'
+        }
+      };
+      mockGet.mockResolvedValue(mockData);
+      const { result } = renderHook(() =>
+        useLoadData(mockUrl, {
+          transformKeys: 'camelCase'
+        })
+      );
+
+      expect(result.current.data).toBeUndefined();
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(() => {
+        expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
+        expect(result.current.data).toEqual({
+          productDetails: {
+            productId: 1,
+            productName: 'Hue'
+          }
+        });
+        expect(result.current.error).toBeUndefined();
+        expect(result.current.loading).toBe(false);
+      });
+    });
+
+    it('should not transform keys when transformKeys is "none" for nested objects', async () => {
+      const mockData = {
+        product_details: {
+          product_id: 1,
+          product_name: 'Hue'
+        }
+      };
+      mockGet.mockResolvedValue(mockData);
+      const { result } = renderHook(() =>
+        useLoadData(mockUrl, {
+          transformKeys: 'none'
+        })
+      );
+
+      expect(result.current.data).toBeUndefined();
+      expect(result.current.error).toBeUndefined();
+      expect(result.current.loading).toBe(true);
+
+      await waitFor(() => {
+        expect(mockGet).toHaveBeenCalledWith(mockUrl, undefined, expect.any(Object));
+        expect(result.current.data).toEqual(mockData);
+        expect(result.current.error).toBeUndefined();
+        expect(result.current.loading).toBe(false);
+      });
+    });
+  });
 });

+ 18 - 3
desktop/core/src/desktop/js/utils/hooks/useLoadData/useLoadData.ts

@@ -17,6 +17,7 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
 import { ApiFetchOptions, get } from '../../../api/utils';
 import { AxiosError } from 'axios';
+import { convertKeysToCamelCase } from '../../../utils/string/changeCasing';
 
 export interface Options<T, U> {
   params?: U;
@@ -25,6 +26,7 @@ export interface Options<T, U> {
   onSuccess?: (data: T) => void;
   onError?: (error: AxiosError) => void;
   pollInterval?: number;
+  transformKeys?: 'camelCase' | 'none';
 }
 
 interface UseLoadDataProps<T> {
@@ -54,6 +56,18 @@ const useLoadData = <T, U = unknown>(
     [localOptions]
   );
 
+  const transformResponse = (data: T): T => {
+    if (options?.transformKeys === 'none') {
+      return data;
+    }
+
+    if (data && (options?.transformKeys === undefined || options?.transformKeys === 'camelCase')) {
+      return convertKeysToCamelCase<T>(data);
+    }
+
+    return data;
+  };
+
   const loadData = useCallback(
     async (isForced: boolean = false) => {
       // Avoid fetching data if the skip option is true
@@ -66,11 +80,12 @@ const useLoadData = <T, U = unknown>(
 
       try {
         const response = await get<T, U>(url, localOptions?.params, fetchOptions);
-        setData(response);
+        const transformedResponse = transformResponse(response);
+        setData(transformedResponse);
         if (localOptions?.onSuccess) {
-          localOptions.onSuccess(response);
+          localOptions.onSuccess(transformedResponse);
         }
-        return response;
+        return transformedResponse;
       } catch (error) {
         setError(error as AxiosError);
         if (localOptions?.onError) {

+ 208 - 0
desktop/core/src/desktop/js/utils/string/changeCasing.test.ts

@@ -0,0 +1,208 @@
+// 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 { convertKeysToCamelCase, toCamelCase, words } from './changeCasing';
+
+describe('words', () => {
+  it('should match words in snake_case string', () => {
+    const input = 'snake_case_example';
+    const result = words(input);
+    expect(result).toEqual(['snake', 'case', 'example']);
+  });
+
+  it('should match words in camelCase string', () => {
+    const input = 'camelCaseExample';
+    const result = words(input);
+    expect(result).toEqual(['camel', 'Case', 'Example']);
+  });
+
+  it('should match words in PascalCase string', () => {
+    const input = 'PascalCaseString';
+    const result = words(input);
+    expect(result).toEqual(['Pascal', 'Case', 'String']);
+  });
+
+  it('should match a string with numbers and words', () => {
+    const input = 'someText123with456numbers';
+    const result = words(input);
+    expect(result).toEqual(['some', 'Text', '123', 'with', '456', 'numbers']);
+  });
+
+  it('should return an empty array for an empty string', () => {
+    const input = '';
+    const result = words(input);
+    expect(result).toEqual([]);
+  });
+
+  it('should match numbers', () => {
+    const input = '1234test567';
+    const result = words(input);
+    expect(result).toEqual(['1234', 'test', '567']);
+  });
+
+  it('should match words in a string with multiple underscores', () => {
+    const input = 'multiple__underscores_test';
+    const result = words(input);
+    expect(result).toEqual(['multiple', 'underscores', 'test']);
+  });
+
+  it('should match a string with numbers, underscores, and mixed case', () => {
+    const input = 'test_123Number_caseTest';
+    const result = words(input);
+    expect(result).toEqual(['test', '123', 'Number', 'case', 'Test']);
+  });
+
+  it('should handle a string with only numbers', () => {
+    const input = '12345';
+    const result = words(input);
+    expect(result).toEqual(['12345']);
+  });
+
+  it('should handle a string with special characters', () => {
+    const input = 'word@123#test!';
+    const result = words(input);
+    expect(result).toEqual(['word', '123', 'test']);
+  });
+});
+
+describe('toCamelCase', () => {
+  it('should convert snake_case to camelCase for simple strings', () => {
+    expect(toCamelCase('snake_case_key')).toBe('snakeCaseKey');
+  });
+
+  it('should handle strings with multiple underscores', () => {
+    expect(toCamelCase('this_is_a_test')).toBe('thisIsATest');
+  });
+
+  it('should handle strings with leading underscores', () => {
+    expect(toCamelCase('_leading_underscore')).toBe('leadingUnderscore');
+  });
+
+  it('should handle strings with trailing underscores', () => {
+    expect(toCamelCase('trailing_underscore_')).toBe('trailingUnderscore');
+  });
+
+  it('should handle strings with multiple leading and trailing underscores', () => {
+    expect(toCamelCase('__trailing__underscore__')).toBe('trailingUnderscore');
+  });
+
+  it('should return the same string if there are no underscores', () => {
+    expect(toCamelCase('alreadyCamelCase')).toBe('alreadyCamelCase');
+  });
+
+  it('should handle empty strings correctly', () => {
+    expect(toCamelCase('')).toBe('');
+  });
+
+  it('should not modify uppercase letters after the first one', () => {
+    expect(toCamelCase('test_STRING_VALUE')).toBe('testStringValue');
+  });
+
+  it('should convert multiple underscores correctly', () => {
+    expect(toCamelCase('multiple__underscores')).toBe('multipleUnderscores');
+  });
+
+  it('should handle numbers in the string', () => {
+    expect(toCamelCase('snake_case_123_test')).toBe('snakeCase123Test');
+  });
+
+  it('should handle mixed-case strings', () => {
+    expect(toCamelCase('MIXED_CASE_example')).toBe('mixedCaseExample');
+  });
+});
+
+describe('convertKeysToCamelCase', () => {
+  it('should convert a single object from snake_case to camelCase', () => {
+    const input = { snake_case_key: 'value', another_snake_case: 'anotherValue' };
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual({
+      snakeCaseKey: 'value',
+      anotherSnakeCase: 'anotherValue'
+    });
+  });
+
+  it('should convert nested objects from snake_case to camelCase', () => {
+    const input = {
+      snake_case_key: 'value',
+      nested_object: { inner_snake_case_key: 'innerValue' }
+    };
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual({
+      snakeCaseKey: 'value',
+      nestedObject: { innerSnakeCaseKey: 'innerValue' }
+    });
+  });
+
+  it('should convert arrays of objects from snake_case to camelCase', () => {
+    const input = [
+      { snake_case_key: 'value1', another_snake_case: 'value2' },
+      { third_snake_case: 'value3' }
+    ];
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual([
+      { snakeCaseKey: 'value1', anotherSnakeCase: 'value2' },
+      { thirdSnakeCase: 'value3' }
+    ]);
+  });
+
+  it('should handle arrays of primitives correctly without changing them', () => {
+    const input = [1, 2, 3];
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual([1, 2, 3]);
+  });
+
+  it('should handle primitive values without modifying them', () => {
+    const input = 'string';
+    const result = convertKeysToCamelCase(input);
+    expect(result).toBe('string');
+  });
+
+  it('should return null for null input', () => {
+    const input = null;
+    const result = convertKeysToCamelCase(input);
+    expect(result).toBeNull();
+  });
+
+  it('should return an empty object if the input is an empty object', () => {
+    const input = {};
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual({});
+  });
+
+  it('should return an empty array if the input is an empty array', () => {
+    const input = [];
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual([]);
+  });
+
+  it('should convert deeply nested objects from snake_case to camelCase', () => {
+    const input = {
+      snake_case_key: {
+        another_snake_case: {
+          deeply_nested_snake_case: 'value'
+        }
+      }
+    };
+    const result = convertKeysToCamelCase(input);
+    expect(result).toEqual({
+      snakeCaseKey: {
+        anotherSnakeCase: {
+          deeplyNestedSnakeCase: 'value'
+        }
+      }
+    });
+  });
+});

+ 53 - 0
desktop/core/src/desktop/js/utils/string/changeCasing.ts

@@ -0,0 +1,53 @@
+// 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.
+
+const wordPattern = new RegExp(
+  ['[A-Z][a-z]+', '[A-Z]+(?=[A-Z][a-z])', '[A-Z]+', '[a-z]+', '[0-9]+'].join('|'),
+  'g'
+);
+
+export const words = (string = '', pattern: RegExp | string = wordPattern): string[] => {
+  return string.match(pattern) || [];
+};
+
+export const toCamelCase = (string = ''): string => {
+  return words(string)
+    .map((word, index) =>
+      index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
+    )
+    .join('');
+};
+
+export const convertKeysToCamelCase = <T>(input: T): T => {
+  // If input is an array, recursively process each item
+  if (Array.isArray(input)) {
+    return input.map(item => convertKeysToCamelCase(item)) as T;
+  }
+
+  // If input is an object, recursively process its keys and values
+  if (input !== null && typeof input === 'object') {
+    return Object.keys(input).reduce((acc, key) => {
+      const camelKey = toCamelCase(key);
+      const value = convertKeysToCamelCase(input[key as keyof typeof input]); // Recursively process values
+      return {
+        ...acc,
+        [camelKey]: value
+      };
+    }, {} as T);
+  }
+
+  return input; // If input is a primitive value, return it as is
+};