Skip to content

Commit

Permalink
feat(DownloadManager): add setting for filename [YTFRONT-3564]
Browse files Browse the repository at this point in the history
  • Loading branch information
KostyaAvtushko committed Jan 29, 2025
1 parent b052987 commit 285d075
Show file tree
Hide file tree
Showing 10 changed files with 678 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,18 @@
&__dsv-separators-item {
padding-top: 10px;
}

&__filename-form {
padding-top: 10px;

&__label {
font-size: 12.6px;

display: block;

margin-bottom: 5px;

letter-spacing: 1px;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {ConnectedProps, connect} from 'react-redux';
import React, {MouseEvent} from 'react';
import {ConnectedProps, connect, useDispatch} from 'react-redux';
import unipika from '../../../../../common/thor/unipika';
import {compose} from 'redux';
import cn from 'bem-cn-lite';
Expand Down Expand Up @@ -38,6 +38,7 @@ import {
getSrcColumns,
} from '../../../../../store/selectors/navigation/content/table';
import {getColumns} from '../../../../../store/selectors/navigation/content/table-ts';
import {downloadFile} from '../../../../../store/actions/navigation/content/table/download-manager';

import './DownloadManager.scss';
import {docsUrl, getExportTableBaseUrl} from '../../../../../config';
Expand All @@ -63,6 +64,13 @@ function checkExcelExporter(cluster: string) {
.catch(() => false);
}

type Error =
| {
inner_errors: string[];
message: string;
}
| undefined;

type ReduxProps = Omit<ConnectedProps<typeof connector>, 'dispatch'>;
type Props = ReduxProps & WithVisibleProps & {className?: string};

Expand All @@ -76,6 +84,7 @@ type State = {
valueSentinel: string;
ysonFormat: 'text' | 'pretty' | 'binary';
columnsMode: 'all' | 'custom';
filename: string;

schemafulDsvMissingMode: 'fail' | 'skip_row' | 'print_sentinel';
separators: {keyValue?: string; record?: string; field?: string};
Expand Down Expand Up @@ -139,6 +148,7 @@ export class DownloadManager extends React.Component<Props, State> {
ysonFormat: 'text',
columnsMode: 'all',
schemafulDsvMissingMode: 'fail', //print_sentinel
filename: this.filename,

separators: {
keyValue: '=',
Expand Down Expand Up @@ -282,6 +292,12 @@ export class DownloadManager extends React.Component<Props, State> {
return path + this.downloadColumns + this.downloadRows;
}

get filename() {
const {path} = this.props;

return path.split('/')[path.split('/').length - 1];
}

getDownloadParams() {
const {transaction_id} = this.props;
const {value: output_format, error} = this.getOutputFormat();
Expand All @@ -300,20 +316,18 @@ export class DownloadManager extends React.Component<Props, State> {

getDownloadLink() {
const {cluster, proxy, externalProxy} = this.props;
const base = makeDirectDownloadPath('read_table', {cluster, proxy, externalProxy});
const {format, number_precision_mode} = this.state;
const {query, error} = this.getDownloadParams();
return {url: base + '?' + query, error};
}

getExcelDownloadLink() {
const {cluster} = this.props;
const base = `${getExportTableBaseUrl({cluster})}/${cluster}/api/export`;
if (format === 'excel') {
const base = `${getExportTableBaseUrl({cluster})}/${cluster}/api/export`;
const params = new URLSearchParams({number_precision_mode});
return {url: `${base}?${params}&${query}`, error};
}

const {query, error} = this.getDownloadParams();
const {number_precision_mode} = this.state;
const params = new URLSearchParams({number_precision_mode});
const base = makeDirectDownloadPath('read_table', {cluster, proxy, externalProxy});

return {url: `${base}?${params}&${query}`, error};
return {url: `${base}?${query}`, error};
}

makeDocsUrl(path = '') {
Expand Down Expand Up @@ -388,6 +402,7 @@ export class DownloadManager extends React.Component<Props, State> {
}

changeFormat = (format: State['format']) => this.setState({format});
changeFilename = (filename: State['filename']) => this.setState({filename});
changeNumRows = (numRows: State['numRows']) => this.setState({numRows});
changeStartRow = (startRow: State['startRow']) => this.setState({startRow});
changeRowsMode = (rowsMode: State['rowsMode']) => this.setState({rowsMode});
Expand Down Expand Up @@ -518,6 +533,17 @@ export class DownloadManager extends React.Component<Props, State> {
);
}

renderFilenameForm() {
const {filename} = this.state;

return (
<div className={block('filename-form')}>
<div className={block('filename-form__label')}>Filename</div>
<TextInput size="m" value={filename} onUpdate={this.changeFilename} />
</div>
);
}

renderRows() {
const {rowsMode, numRows} = this.state;

Expand Down Expand Up @@ -852,33 +878,25 @@ export class DownloadManager extends React.Component<Props, State> {
{format === 'yson' && this.renderYson()}
{format === 'excel' && this.renderExcel()}
</div>
{this.renderFilenameForm()}
</div>
</div>
</div>
);
}

renderConfirmButton = (className: string) => {
const {format} = this.state;

const {url, error} =
format === 'excel' ? this.getExcelDownloadLink() : this.getDownloadLink();

renderModalConfirmButton(classNameConfirm: string) {
const {filename} = this.state;
const {url, error} = this.getDownloadLink();
return (
<Button
size="m"
view="action"
className={className}
title="Download"
target="_blank"
href={url}
disabled={Boolean(error)}
qa="download-static-table"
>
Download
</Button>
<ConfirmButton
className={classNameConfirm}
filename={filename}
url={url}
error={error}
/>
);
};
}

showDialog = () => {
const {handleShow, cluster} = this.props;
Expand Down Expand Up @@ -912,14 +930,41 @@ export class DownloadManager extends React.Component<Props, State> {
onCancel={handleClose}
confirmText="Download"
content={this.renderContent()}
renderCustomConfirm={this.renderConfirmButton}
renderCustomConfirm={this.renderModalConfirmButton.bind(this)}
/>
)}
</div>
);
}
}

function ConfirmButton(props: {className?: string; filename: string; url: string; error: Error}) {
const {url, className, filename, error} = props;

const dispatch = useDispatch();

const handleClick = (e: MouseEvent) => {
e.preventDefault();
dispatch(downloadFile(url, filename));
};

return (
<Button
size="m"
view="action"
className={className}
title="Download"
target="_blank"
onClick={handleClick}
href={url}
disabled={Boolean(error)}
qa="download-static-table"
>
Download
</Button>
);
}

const mapStateToProps = (state: RootState) => {
const {loading}: {loading: boolean} = state.navigation.content.table;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, {useEffect, useState} from 'react';
import moment, {Moment} from 'moment';
import {useSelector} from 'react-redux';

import format from '../../../../../../common/hammer/format';
import MetaTable from '../../../../../../components/MetaTable/MetaTable';

import {RootState} from '../../../../../../store/reducers';
import {getDownloadTableInfo} from '../../../../../../store/selectors/navigation/content/download-manager';

interface Props {
id: string;
filename: string;
}

export function DownloadShortInfo({id, filename}: Props) {
const [time, setTime] = useState<Moment>(moment());
const {startTime, loading, loaded} = useSelector((state: RootState) =>
getDownloadTableInfo(state, id),
);

useEffect(() => {
if (loaded && !loading) return;
const interval = setInterval(() => {
setTime(moment());
}, 1000);

return () => clearInterval(interval);
}, [loaded, loading]);

const diff = moment(time).diff(startTime);

return (
<MetaTable
items={[
{
key: 'Filename',
value: filename,
visible: Boolean(filename),
},
{
key: 'Duration',
value: format.TimeDuration(diff),
visible: Boolean(format.TimeDuration(diff)),
},
]}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import {ThunkAction} from 'redux-thunk';
import {UnknownAction} from 'redux';
import {Text, Toaster} from '@gravity-ui/uikit';
import axios from 'axios';

import {AppStoreProvider} from '../../../../../containers/App/AppStoreProvider';
import {DownloadShortInfo} from '../../../../../pages/navigation/content/Table/DownloadManager/DownloadShortInfo/DownloadShortInfo';

import {RootState} from '../../../../reducers';
import {downloadManagerActions} from '../../../../reducers/navigation/content/table/download-manager';

import {downloadFileFromResponse} from '../../../../../utils/download-file';

const requestDownloadFile = async (url: string) =>
axios({
method: 'get',
url: url,
responseType: 'blob',
withCredentials: true,
});

const toaster = new Toaster();

const HIDING_TIMING = 5000;

export const downloadFile = (
url: string,
filename: string,
): ThunkAction<Promise<void>, RootState, any, UnknownAction> => {
const id = crypto.randomUUID();

return async (dispatch, _getState) => {
try {
dispatch(downloadManagerActions.onRequest({id}));
updateToaster(id, 'loading', {filename});

const response = await requestDownloadFile(url);

downloadFileFromResponse(filename, response);

dispatch(downloadManagerActions.onSuccess({id}));
updateToaster(id, 'success');
} catch (error: any) {
let errorMessage = error;
if (error.response) {
const errorText = await error.response.data.text();
errorMessage = JSON.parse(errorText).message;
}

dispatch(downloadManagerActions.onFailure({id, error}));
updateToaster(id, 'failure', {errorMessage});
}

setTimeout(() => {
dispatch(downloadManagerActions.onCleanup({id}));
}, HIDING_TIMING * 2);
};
};

const updateToaster = (
id: string,
status: 'loading' | 'success' | 'failure',
options?: {
filename?: string;
errorMessage?: string;
},
) => {
if (status === 'loading') {
toaster.add({
title: 'Downloading',
name: `downloadingFile_${id}`,
theme: 'info',
content: (
<AppStoreProvider>
<DownloadShortInfo id={id} filename={options?.filename || ''} />
</AppStoreProvider>
),
autoHiding: false,
});
}

if (status === 'success') {
toaster.update(`downloadingFile_${id}`, {
title: 'Success',
theme: 'success',
autoHiding: HIDING_TIMING,
});
}

if (status === 'failure') {
toaster.update(`downloadingFile_${id}`, {
title: 'Failure',
theme: 'danger',
content: <Text>{options?.errorMessage || ''}</Text>,
autoHiding: HIDING_TIMING,
});
}
};
2 changes: 2 additions & 0 deletions packages/ui/src/ui/store/reducers/navigation/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import file from './file';
import replicatedTable from './replicated-table';
import transaction from './transaction';
import transactionMap from './transaction-map/transaction-map';
import {downloadManager} from './table/download-manager';

export default combineReducers({
downloadManager,
replicatedTable,
mapNode,
table,
Expand Down
Loading

0 comments on commit 285d075

Please sign in to comment.