Skip to content

Commit b293ad7

Browse files
Merge branch 'master' into Overview
2 parents 4c9d614 + f576014 commit b293ad7

24 files changed

+758
-76
lines changed

.coveragerc

-2
This file was deleted.

.github/workflows/commitflow-py3.yml

+5-7
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,19 @@ jobs:
6161
6262
- name: run tests
6363
run: |
64-
mkdir -p test-reports # Ensure the directory exists
65-
mkdir -p code-coverage-reports # Ensure the directory exists
66-
./build/env/bin/pytest --cov=about --cov=beeswax --cov=filebrowser --cov=hbase --cov=help --cov=hive --cov=impala --cov=jobbrowser --cov=jobsub --cov=metastore --cov=oozie --cov=pig --cov=proxy --cov=rdbms --cov=search --cov=security --cov=spark --cov=sqoop --cov=useradmin --cov=zookeeper --cov=desktop --cov=aws --cov=azure --cov=dashboard --cov=hadoop --cov=indexer --cov=kafka --cov=libanalyze --cov=liboauth --cov=liboozie --cov=librdbms --cov=libsaml --cov=libsentry --cov=libsolr --cov=libzookeeper --cov=metadata --cov=notebook --cov-report=html:code-coverage-report --html=test-reports/report_${{ matrix.python-version }}.html --self-contained-html
64+
./build/env/bin/pytest
6765
6866
- name: upload pytest report
6967
uses: actions/upload-artifact@v4
7068
with:
71-
name: test-reports-${{ matrix.python-version }}
72-
path: test-reports
69+
name: hue-pytest-report-${{ matrix.python-version }}
70+
path: reports/pytest
7371

7472
- name: upload code coverage report
7573
uses: actions/upload-artifact@v4
7674
with:
77-
name: code-coverage-report-${{ matrix.python-version }}
78-
path: code-coverage-report
75+
name: hue-code-cov-report-${{ matrix.python-version }}
76+
path: reports/code-cov
7977

8078
- name: Check and comment if no unit test files are modified
8179
if: matrix.python-version == '3.11'

.gitignore

+6-5
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@ desktop/core/ext-py/lxml/src/lxml/lxml-version.h
6161
# Nose --with-id's file
6262
/.noseids
6363

64-
# Coverage product
65-
/.coverage
66-
/coverage.xml
67-
/coverage
64+
# Coverage reports
65+
.coverage*
66+
coverage.xml
67+
68+
# Pytest reports
69+
report.html
6870

6971
# Common utility alias
7072
/d
@@ -98,7 +100,6 @@ desktop/desktop-test.db
98100
ext/hadoop/
99101
ext/hive/
100102
ext/oozie/
101-
report.html
102103
oozie.sql
103104
tools/jenkins/ext/
104105
tools/jenkins/oozie.sql

desktop/core/src/desktop/api_public.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ def storage_trash_bulk_restore(request):
371371
return filebrowser_api.bulk_op(django_request, filebrowser_api.trash_restore)
372372

373373

374-
@api_view(["DELETE"])
374+
@api_view(["POST"])
375375
def storage_trash_purge(request):
376376
django_request = get_django_request(request)
377377
return filebrowser_api.trash_purge(django_request)

desktop/core/src/desktop/js/apps/storageBrowser/StorageBrowserTab/StorageBrowserTab.tsx

+52-5
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import BucketIcon from '@cloudera/cuix-core/icons/react/BucketIcon';
2121

2222
import PathBrowser from '../../../reactComponents/PathBrowser/PathBrowser';
2323
import StorageDirectoryPage from '../StorageDirectoryPage/StorageDirectoryPage';
24-
import { FILE_STATS_API_URL } from '../api';
25-
import { BrowserViewType, FileStats, FileSystem } from '../types';
24+
import { FILE_STATS_API_URL, TRASH_PATH } from '../api';
25+
import { BrowserViewType, FileStats, FileSystem, TrashPath } from '../types';
2626
import useLoadData from '../../../utils/hooks/useLoadData/useLoadData';
2727
import { BorderlessButton } from 'cuix/dist/components/Button';
2828

@@ -33,6 +33,9 @@ import LoadingErrorWrapper from '../../../reactComponents/LoadingErrorWrapper/Lo
3333
import { getFileSystemAndPath } from '../../../reactComponents/PathBrowser/PathBrowser.util';
3434
import RefreshIcon from '@cloudera/cuix-core/icons/react/RefreshIcon';
3535
import HomeIcon from '@cloudera/cuix-core/icons/react/HomeIcon';
36+
import DeleteIcon from '@cloudera/cuix-core/icons/react/DeleteIcon';
37+
import { inTrash } from '../../../utils/storageBrowserUtils';
38+
import { Alert } from 'antd';
3639

3740
interface StorageBrowserTabProps {
3841
fileSystem: FileSystem;
@@ -57,6 +60,27 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
5760

5861
const { t } = i18nReact.useTranslation();
5962

63+
const {
64+
data: trashPath,
65+
loading: trashLoading,
66+
reloadData: onTrashPathReload
67+
} = useLoadData<TrashPath>(TRASH_PATH, {
68+
params: { path: fileSystem.user_home_directory },
69+
skip: !fileSystem.config?.is_trash_enabled || !fileSystem.user_home_directory
70+
});
71+
72+
const onTrashClick = async () => {
73+
const latestTrashData = await onTrashPathReload();
74+
setFilePath(latestTrashData?.trash_path ?? '');
75+
};
76+
77+
const reloadTrashPath = () => {
78+
if (trashPath?.trash_path) {
79+
return;
80+
}
81+
onTrashPathReload();
82+
};
83+
6084
const {
6185
data: fileStats,
6286
loading,
@@ -96,8 +120,10 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
96120
}
97121
];
98122

123+
const isLoading = loading || trashLoading;
124+
99125
return (
100-
<LoadingErrorWrapper loading={loading} errors={errorConfig}>
126+
<LoadingErrorWrapper loading={isLoading} errors={errorConfig}>
101127
<div className="hue-storage-browser-tab-content" data-testid={testId}>
102128
<div className="hue-storage-browser__title-bar" data-testid={`${testId}-title-bar`}>
103129
<div className="hue-storage-browser__title">
@@ -118,6 +144,18 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
118144
>
119145
{t('Home')}
120146
</BorderlessButton>
147+
{fileSystem.config?.is_trash_enabled && (
148+
<BorderlessButton
149+
onClick={onTrashClick}
150+
className="hue-path-browser__home-btn"
151+
data-event=""
152+
title={t('Trash')}
153+
icon={<DeleteIcon />}
154+
disabled={!trashPath?.trash_path}
155+
>
156+
{t('Trash')}
157+
</BorderlessButton>
158+
)}
121159
<BorderlessButton
122160
onClick={() => reloadData()}
123161
className="hue-storage-browser__home-bar-btns"
@@ -129,6 +167,14 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
129167
</BorderlessButton>
130168
</div>
131169
</div>
170+
{!!inTrash(filePath) && fileSystem.file_system === 'hdfs' && (
171+
<Alert
172+
type="warning"
173+
message={t(
174+
'This is Hadoop trash. Files will be under a checkpoint, or timestamp named, directory.'
175+
)}
176+
/>
177+
)}
132178
<div
133179
className="hue-storage-browser__path-browser-panel"
134180
data-testid={`${testId}-path-browser-panel`}
@@ -140,14 +186,15 @@ const StorageBrowserTab = ({ fileSystem, testId }: StorageBrowserTabProps): JSX.
140186
showIcon={false}
141187
/>
142188
</div>
143-
{fileStats?.type === BrowserViewType.dir && !loading && (
189+
{fileStats?.type === BrowserViewType.dir && !isLoading && (
144190
<StorageDirectoryPage
145191
fileStats={fileStats}
146192
onFilePathChange={setFilePath}
147193
fileSystem={fileSystem}
194+
reloadTrashPath={reloadTrashPath}
148195
/>
149196
)}
150-
{fileStats?.type === BrowserViewType.file && !loading && (
197+
{fileStats?.type === BrowserViewType.file && !isLoading && (
151198
<StorageFilePage fileName={fileName} fileStats={fileStats} onReload={reloadData} />
152199
)}
153200
</div>
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { render, screen } from '@testing-library/react';
1919
import userEvent from '@testing-library/user-event';
2020
import '@testing-library/jest-dom';
2121

22-
import StorageBrowserActions from './StorageBrowserActions';
22+
import FileAndFolderActions from './FileAndFolderActions';
2323
import { StorageDirectoryTableData } from '../../../types';
2424
import { get } from '../../../../../api/utils';
2525
import huePubSub from '../../../../../utils/huePubSub';
@@ -44,7 +44,7 @@ jest.mock('config/hueConfig', () => ({
4444

4545
const mockGet = get as jest.MockedFunction<typeof get>;
4646

47-
describe('StorageBrowserRowActions', () => {
47+
describe('FileAndFolderActions', () => {
4848
//View summary option is enabled and added to the actions menu when the row data is either hdfs/ofs and a single file
4949
const mockTwoRecords: StorageDirectoryTableData[] = [
5050
{
@@ -74,7 +74,16 @@ describe('StorageBrowserRowActions', () => {
7474
const mockRecord: StorageDirectoryTableData = mockTwoRecords[0];
7575

7676
const setLoadingFiles = jest.fn();
77-
const onSuccessfulAction = jest.fn();
77+
const mockOnActionSuccess = jest.fn();
78+
const mockOnActionError = jest.fn();
79+
const mockConfig = {
80+
is_trash_enabled: true,
81+
is_hdfs_superuser: true,
82+
groups: ['hue'],
83+
users: ['hue'],
84+
superuser: 'hue',
85+
supergroup: 'hue'
86+
};
7887

7988
const setUpActionMenu = async (
8089
records: StorageDirectoryTableData[],
@@ -93,9 +102,11 @@ describe('StorageBrowserRowActions', () => {
93102
]
94103
: records;
95104
const { getByRole } = render(
96-
<StorageBrowserActions
105+
<FileAndFolderActions
106+
config={mockConfig}
107+
onActionError={mockOnActionError}
97108
setLoadingFiles={setLoadingFiles}
98-
onSuccessfulAction={onSuccessfulAction}
109+
onActionSuccess={mockOnActionSuccess}
99110
selectedFiles={selectedFiles}
100111
currentPath="/path/to/folder"
101112
/>
+4-4
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ import ConfigureIcon from '@cloudera/cuix-core/icons/react/ConfigureIcon';
3434

3535
import { i18nReact } from '../../../../../utils/i18nReact';
3636
import huePubSub from '../../../../../utils/huePubSub';
37-
import './StorageBrowserActions.scss';
37+
import './FileAndFolderActions.scss';
3838
import { FileStats, FileSystem, StorageDirectoryTableData } from '../../../types';
39-
import { ActionType, getEnabledActions } from './StorageBrowserActions.util';
39+
import { ActionType, getEnabledActions } from './FileAndFolderActions.util';
4040
import MoveCopyModal from './MoveCopyModal/MoveCopyModal';
4141
import RenameModal from './RenameModal/RenameModal';
4242
import ReplicationModal from './ReplicationModal/ReplicationModal';
@@ -71,7 +71,7 @@ const iconsMap: Record<ActionType, JSX.Element> = {
7171
[ActionType.ChangePermission]: <ConfigureIcon />
7272
};
7373

74-
const StorageBrowserActions = ({
74+
const FileAndFolderActions = ({
7575
config,
7676
currentPath,
7777
selectedFiles,
@@ -217,4 +217,4 @@ const StorageBrowserActions = ({
217217
);
218218
};
219219

220-
export default StorageBrowserActions;
220+
export default FileAndFolderActions;

desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import React from 'react';
1818
import { render, fireEvent } from '@testing-library/react';
1919
import '@testing-library/jest-dom';
2020
import MoveCopyModal from './MoveCopyModal';
21-
import { ActionType } from '../StorageBrowserActions.util';
21+
import { ActionType } from '../FileAndFolderActions.util';
2222
import { BULK_COPY_API_URL, BULK_MOVE_API_URL } from '../../../../api';
2323
import { StorageDirectoryTableData } from '../../../../types';
2424

desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/MoveCopyModal/MoveCopyModal.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import React from 'react';
1818
import { i18nReact } from '../../../../../../utils/i18nReact';
1919
import useSaveData from '../../../../../../utils/hooks/useSaveData/useSaveData';
20-
import { ActionType } from '../StorageBrowserActions.util';
20+
import { ActionType } from '../FileAndFolderActions.util';
2121
import { BULK_COPY_API_URL, BULK_MOVE_API_URL } from '../../../../api';
2222
import FileChooserModal from '../../../../FileChooserModal/FileChooserModal';
2323
import { FileStats, StorageDirectoryTableData } from '../../../../types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Licensed to Cloudera, Inc. under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. Cloudera, Inc. licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
import React from 'react';
18+
import { render } from '@testing-library/react';
19+
import '@testing-library/jest-dom';
20+
import StorageDirectoryActions from './StorageDirectoryActions';
21+
22+
const mockOnActionSuccess = jest.fn();
23+
const mockSetLoadingFiles = jest.fn();
24+
const mockOnFilesDrop = jest.fn();
25+
const mockOnFilePathChange = jest.fn();
26+
27+
describe('StorageDirectoryActions Component', () => {
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
const mockSelectedFiles = [
33+
{
34+
name: 'file1.txt',
35+
size: '0 Byte',
36+
type: 'file',
37+
permission: 'rwxrwxrwx',
38+
mtime: '2021-01-01 00:00:00',
39+
path: '/user/path/.Trash/Current/file1.txt',
40+
user: 'test',
41+
group: 'test',
42+
replication: 1
43+
}
44+
];
45+
46+
const mockFileStats = {
47+
name: 'file1.txt',
48+
type: 'file',
49+
permission: 'rwxrwxrwx',
50+
mtime: 123,
51+
path: '/user/path/file1.txt',
52+
user: 'test',
53+
group: 'test',
54+
replication: 1,
55+
atime: 123,
56+
blockSize: 123,
57+
mode: 123,
58+
rwx: 'rwxrwxrwx',
59+
size: 123
60+
};
61+
62+
const mockFileSystem = {
63+
file_system: 'hdfs',
64+
user_home_directory: '/user/hue'
65+
};
66+
67+
const mockTrashPath = '/user/path/.Trash/Current';
68+
const mockIsTrashEmpty = !mockSelectedFiles.length;
69+
70+
it('should render the Trash actions when path is in Trash', () => {
71+
const { getByRole, queryByRole } = render(
72+
<StorageDirectoryActions
73+
selectedFiles={mockSelectedFiles}
74+
isFolderEmpty={mockIsTrashEmpty}
75+
onActionSuccess={mockOnActionSuccess}
76+
setLoadingFiles={mockSetLoadingFiles}
77+
fileStats={{ ...mockFileStats, path: mockTrashPath }}
78+
fileSystem={mockFileSystem}
79+
onFilePathChange={mockOnFilePathChange}
80+
onFilesDrop={mockOnFilesDrop}
81+
/>
82+
);
83+
84+
expect(getByRole('button', { name: 'Restore' })).toBeInTheDocument();
85+
expect(getByRole('button', { name: 'Empty trash' })).toBeInTheDocument();
86+
expect(queryByRole('button', { name: 'Actions' })).not.toBeInTheDocument();
87+
expect(queryByRole('button', { name: 'New' })).not.toBeInTheDocument();
88+
});
89+
90+
it('should render the regular actions when path is not in Trash', () => {
91+
const { getByRole, queryByRole } = render(
92+
<StorageDirectoryActions
93+
selectedFiles={mockSelectedFiles}
94+
isFolderEmpty={mockIsTrashEmpty}
95+
onActionSuccess={mockOnActionSuccess}
96+
setLoadingFiles={mockSetLoadingFiles}
97+
fileStats={mockFileStats}
98+
fileSystem={mockFileSystem}
99+
onFilePathChange={mockOnFilePathChange}
100+
onFilesDrop={mockOnFilesDrop}
101+
/>
102+
);
103+
104+
expect(getByRole('button', { name: 'Actions' })).toBeInTheDocument();
105+
expect(getByRole('button', { name: 'New' })).toBeInTheDocument();
106+
expect(queryByRole('button', { name: 'Restore' })).not.toBeInTheDocument();
107+
expect(queryByRole('button', { name: 'Empty trash' })).not.toBeInTheDocument();
108+
});
109+
});

0 commit comments

Comments
 (0)