diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserPage.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserPage.scss index dadbc269583..5a4a93c96da 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserPage.scss +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserPage.scss @@ -32,8 +32,22 @@ } .hue-storage-browser__tab { + display: flex; + height: 100%; background-color: vars.$fluidx-gray-100; padding: 0 16px; - height: 100%; + + .ant-tabs-content-holder { + display: flex; + flex: 1; + overflow: auto; + + .ant-tabs-content, + .ant-tabs-tabpane-active { + display: flex; + flex-direction: column; + flex: 1; + } + } } -} +} \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.scss index 7d4cafc69a6..8c2033999fc 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.scss +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.scss @@ -17,17 +17,19 @@ @use 'mixins'; .antd.cuix { - .hue-storage-browser-tabContent { - margin: vars.$fluidx-spacing-s 0; + .hue-storage-browser-tab-content { + display: flex; + flex: 1; + flex-direction: column; + height: 100%; } .hue-storage-browser__title-bar { display: flex; - margin: 0 vars.$fluidx-spacing-s; + gap: vars.$fluidx-spacing-xs; } .hue-storage-browser__icon { - margin-right: vars.$fluidx-spacing-xs; flex: 0 0 auto; height: vars.$fluidx-heading-h3-line-height; } @@ -41,13 +43,12 @@ .hue-storage-browser__path-browser-panel { display: flex; - margin: vars.$fluidx-spacing-xs 0; align-items: center; + gap: vars.$fluidx-spacing-xs; } .hue-storage-browser__filePath { flex: 0 0 auto; font-weight: 600; - margin: 0 vars.$fluidx-spacing-xs 0 vars.$fluidx-spacing-s; } -} +} \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx index 89eadebb04d..24cb46a570b 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTabContents/StorageBrowserTabContent.tsx @@ -23,11 +23,16 @@ import BucketIcon from '@cloudera/cuix-core/icons/react/BucketIcon'; import PathBrowser from '../../../../reactComponents/FileChooser/PathBrowser/PathBrowser'; import StorageBrowserTable from '../StorageBrowserTable/StorageBrowserTable'; import { VIEWFILES_API_URl } from '../../../../reactComponents/FileChooser/api'; -import { PathAndFileData, SortOrder } from '../../../../reactComponents/FileChooser/types'; +import { + BrowserViewType, + PathAndFileData, + SortOrder +} from '../../../../reactComponents/FileChooser/types'; import { DEFAULT_PAGE_SIZE } from '../../../../utils/constants/storageBrowser'; import useLoadData from '../../../../utils/hooks/useLoadData'; import './StorageBrowserTabContent.scss'; +import StorageFilePage from '../../StorageFilePage/StorageFilePage'; interface StorageBrowserTabContentProps { user_home_dir: string; @@ -35,7 +40,7 @@ interface StorageBrowserTabContentProps { } const defaultProps = { - testId: 'hue-storage-browser-tabContent' + testId: 'hue-storage-browser-tab-content' }; const StorageBrowserTabContent = ({ @@ -69,11 +74,11 @@ const StorageBrowserTabContent = ({ return ( -
+

- {filesData?.breadcrumbs[filesData?.breadcrumbs?.length - 1].label} + {filesData?.path?.split('/').pop()}

- + {filesData?.type === BrowserViewType.file ? ( + + ) : ( + + )}
); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.scss b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.scss index 22d84d33b1c..f657999b99e 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.scss +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.scss @@ -24,7 +24,7 @@ $icon-margin: 5px; .antd.cuix { .hue-storage-browser__actions-bar { display: flex; - margin: vars.$fluidx-spacing-s; + margin: vars.$fluidx-spacing-s 0; justify-content: space-between; } @@ -46,8 +46,6 @@ $icon-margin: 5px; } .hue-storage-browser__table { - margin: 0 vars.$fluidx-spacing-s; - .ant-table-placeholder { height: $table-placeholder-height; text-align: center; @@ -65,6 +63,10 @@ $icon-margin: 5px; } .hue-storage-browser__table-row { + :hover { + cursor: pointer; + } + td.ant-table-cell { height: $cell-height; @include mixins.nowrap-ellipsis; @@ -87,4 +89,4 @@ $icon-margin: 5px; .hue-storage-browser__actions-dropdown { width: $action-dropdown-width; @include mixins.hue-svg-icon__d3-conflict; -} +} \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx index 4d55a37eebe..f1634d3c440 100644 --- a/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserPage/StorageBrowserTable/StorageBrowserTable.tsx @@ -14,11 +14,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState, useCallback, useEffect } from 'react'; import { ColumnProps } from 'antd/lib/table'; -import { Dropdown, Input, Spin } from 'antd'; +import { Dropdown, Input, Spin, Tooltip } from 'antd'; import { MenuItemGroupType } from 'antd/lib/menu/hooks/useItems'; -import Tooltip from 'antd/es/tooltip'; import FolderIcon from '@cloudera/cuix-core/icons/react/ProjectIcon'; import SortAscending from '@cloudera/cuix-core/icons/react/SortAscendingIcon'; @@ -39,14 +38,15 @@ import { mkdir, touch } from '../../../../reactComponents/FileChooser/api'; import { StorageBrowserTableData, SortOrder, - PathAndFileData, - BrowserViewType + PathAndFileData } from '../../../../reactComponents/FileChooser/types'; import Pagination from '../../../../reactComponents/Pagination/Pagination'; import StorageBrowserActions from '../StorageBrowserActions/StorageBrowserActions'; import InputModal from '../../InputModal/InputModal'; +import formatBytes from '../../../../utils/formatBytes'; import './StorageBrowserTable.scss'; +import { formatTimestamp } from '../../../../utils/dateTimeUtils'; interface StorageBrowserTableProps { className?: string; @@ -91,29 +91,28 @@ const StorageBrowserTable = ({ ...restProps }: StorageBrowserTableProps): JSX.Element => { const [loadingFiles, setLoadingFiles] = useState(false); - const [tableHeight, setTableHeight] = useState(); + const [tableHeight, setTableHeight] = useState(100); const [selectedFiles, setSelectedFiles] = useState([]); const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewFileModal, setShowNewFileModal] = useState(false); - const [viewType, setViewType] = useState(BrowserViewType.dir); const { t } = i18nReact.useTranslation(); const tableData: StorageBrowserTableData[] = useMemo(() => { - return ( - filesData?.files - ?.filter(file => !['.', '..'].includes(file.name)) // removes ..(previous folder) and .(current folder) - .map(file => ({ - name: file.name, - size: file.humansize, - user: file.stats.user, - group: file.stats.group, - permission: file.rwx, - mtime: file.mtime, - type: file.type, - path: file.path - })) ?? [] - ); + if (!filesData?.files) { + return []; + } + + return filesData?.files?.map(file => ({ + name: file.name, + size: formatBytes(file.stats?.size), + user: file.stats.user, + group: file.stats.group, + permission: file.rwx, + mtime: file.stats?.mtime ? formatTimestamp(new Date(Number(file.stats.mtime) * 1000)) : '-', + type: file.type, + path: file.path + })); }, [filesData]); const newActionsMenuItems: MenuItemGroupType[] = [ @@ -171,7 +170,7 @@ const StorageBrowserTable = ({ const getColumns = (file: StorageBrowserTableData) => { const columns: ColumnProps[] = []; - for (const [key] of Object.entries(file)) { + for (const key of Object.keys(file)) { const column: ColumnProps = { dataIndex: key, title: ( @@ -195,9 +194,8 @@ const StorageBrowserTable = ({ }; if (key === 'name') { column.width = '40%'; - //TODO: Apply tooltip only for truncated values column.render = (_, record: StorageBrowserTableData) => ( - + {record.type === 'dir' ? : } @@ -302,14 +300,6 @@ const StorageBrowserTable = ({ }; }, []); - useEffect(() => { - if (filesData?.type === 'file') { - setViewType(BrowserViewType.file); - } else { - setViewType(BrowserViewType.dir); - } - }, [filesData]); - const locale = { emptyText: t('Folder is empty') }; @@ -326,56 +316,45 @@ const StorageBrowserTable = ({ }} />
- {viewType === BrowserViewType.dir && ( - <> - - - - {t('New')} - - - - - )} + + + + {t('New')} + + +
- {viewType === BrowserViewType.dir && ( - record.path + '' + index} - rowSelection={{ - type: 'checkbox', - ...rowSelection - }} - scroll={{ y: tableHeight }} - data-testid={`${testId}`} - locale={locale} - {...restProps} - /> - )} - - {viewType === BrowserViewType.file && ( - // TODO: code for file view -
File view
- )} +
record.path + '' + index} + rowSelection={{ + type: 'checkbox', + ...rowSelection + }} + scroll={{ y: tableHeight }} + data-testid={`${testId}`} + locale={locale} + {...restProps} + /> {filesData?.page && ( ({ + ...jest.requireActual('../../../utils/dateTimeUtils'), + formatTimestamp: () => { + return 'April 8, 2021 at 00:00 AM'; + } +})); + +// Mock data for fileData +const mockFileData: PathAndFileData = { + path: '/path/to/file.txt', + stats: { + size: 123456, + user: 'testuser', + group: 'testgroup', + mtime: '1617877200', + atime: '1617877200', + mode: 33188, + path: '/path/to/file.txt', + aclBit: false + }, + rwx: 'rwxr-xr-x', + breadcrumbs: [], + view: { + contents: 'Initial file content' + }, + files: [], + page: { + number: 1, + num_pages: 1, + previous_page_number: 1, + next_page_number: 1, + start_index: 1, + end_index: 1, + total_count: 1 + }, + pagesize: 100 +}; + +describe('StorageFilePage', () => { + it('renders file metadata and content', () => { + render(); + + expect(screen.getByText('Size')).toBeInTheDocument(); + expect(screen.getByText('120.56 KB')).toBeInTheDocument(); + expect(screen.getByText('Created By')).toBeInTheDocument(); + expect(screen.getByText('testuser')).toBeInTheDocument(); + expect(screen.getByText('Group')).toBeInTheDocument(); + expect(screen.getByText('testgroup')).toBeInTheDocument(); + expect(screen.getByText('Permissions')).toBeInTheDocument(); + expect(screen.getByText('rwxr-xr-x')).toBeInTheDocument(); + expect(screen.getByText('Last Modified')).toBeInTheDocument(); + expect(screen.getByText('April 8, 2021 at 00:00 AM')).toBeInTheDocument(); + expect(screen.getByText('Content')).toBeInTheDocument(); + expect(screen.getByText('Initial file content')).toBeInTheDocument(); + }); + + it('shows edit button and hides save/cancel buttons initially', () => { + render(); + + expect(screen.getByRole('button', { name: 'Edit' })).toBeVisible(); + expect(screen.queryByRole('button', { name: 'Save' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull(); + }); + + it('shows save and cancel buttons when editing', async () => { + const user = userEvent.setup(); + render(); + + expect(screen.getByRole('button', { name: 'Edit' })).toBeVisible(); + expect(screen.queryByRole('button', { name: 'Save' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull(); + + await user.click(screen.getByRole('button', { name: 'Edit' })); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Save' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible(); + }); + + expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull(); + }); + + it('updates textarea value and calls handleSave', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Edit' })); + + const textarea = screen.getByRole('textbox'); + expect(textarea).toBeVisible(); + + await user.clear(textarea); + await user.type(textarea, 'Updated file content'); + + expect(textarea).toHaveValue('Updated file content'); + + await user.click(screen.getByRole('button', { name: 'Save' })); + + expect(screen.getByText('Updated file content')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Save' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull(); + expect(screen.getByRole('button', { name: 'Edit' })).toBeVisible(); + }); + + it('cancels editing and reverts textarea value', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button', { name: 'Edit' })); + + const textarea = screen.getByRole('textbox'); + expect(textarea).toBeVisible(); + + await user.clear(textarea); + await user.type(textarea, 'Updated file content'); + expect(textarea).toHaveValue('Updated file content'); + + await user.click(screen.getByRole('button', { name: 'Cancel' })); + expect(textarea).toHaveValue('Initial file content'); + expect(screen.queryByRole('button', { name: 'Save' })).toBeNull(); + expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull(); + expect(screen.getByRole('button', { name: 'Edit' })).toBeVisible(); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx new file mode 100644 index 00000000000..43fab39d3ce --- /dev/null +++ b/desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx @@ -0,0 +1,104 @@ +// 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 React, { useMemo } from 'react'; +import { PathAndFileData } from '../../../reactComponents/FileChooser/types'; +import './StorageFilePage.scss'; +import { i18nReact } from '../../../utils/i18nReact'; +import Button, { PrimaryButton } from 'cuix/dist/components/Button'; +import { getFileMetaData } from './StorageFilePage.util'; + +const StorageFilePage = ({ fileData }: { fileData: PathAndFileData }): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const [isEditing, setIsEditing] = React.useState(false); + const [fileContent, setFileContent] = React.useState(fileData.view?.contents); + const fileMetaData = useMemo(() => getFileMetaData(t, fileData), [t, fileData]); + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleSave = () => { + // TODO: Save file content to API + setIsEditing(false); + }; + + const handleCancel = () => { + setIsEditing(false); + setFileContent(fileData.view?.contents); + }; + + return ( +
+
+ {fileMetaData.map((row, index) => ( +
+ {row.map(item => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+ ))} +
+ +
+
+ {t('Content')} +
+ + + +
+
+ +