From 6710c0b67ff012f3ebec4de3108c25827177f8a5 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Wed, 11 Dec 2024 12:51:33 +0300 Subject: [PATCH] feat(Attributes): add download button [YTFRONT-4310] --- .../AttributesModal/AttributesModal.js | 16 +++++++++ .../DownloadFileButton.tsx | 30 ++++++++++++++++ .../YsonDownloadButton.tsx | 22 ++++++++++++ .../helpers/attributesToString.ts | 13 +++++++ .../DownloadAttributesButton/index.ts | 2 ++ .../src/ui/components/Modal/SimpleModal.tsx | 8 +++++ .../ChytPageClique/ChytPageCliqueSpeclet.tsx | 8 +++++ .../NodeUnrecognizedOptions.tsx | 8 +++++ .../ui/pages/job/JobGeneral/JobGeneral.tsx | 14 +++++++- .../job/tabs/Specification/Specification.tsx | 16 +++++++-- .../navigation/helpers/pathToFileName.ts | 3 ++ .../navigation/tabs/Attributes/Attributes.js | 27 ++++++++++++-- .../tabs/Flow/PipelineSpec/PipelineSpec.tsx | 7 ++++ .../TableMountConfig/TableMountConfig.tsx | 18 +++++++++- .../tabs/UserAttributes/UserAttributes.js | 15 +++++++- .../ExperimentAssignments.tsx | 24 +++++++++++-- .../tabs/attributes/OperationAttributes.tsx | 19 +++++++++- .../tabs/specification/Specification.js | 34 +++++++++++++++--- ...map-node---Attributes-1-chromium-linux.png | Bin 76884 -> 78229 bytes 19 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 packages/ui/src/ui/components/DownloadAttributesButton/DownloadFileButton.tsx create mode 100644 packages/ui/src/ui/components/DownloadAttributesButton/YsonDownloadButton.tsx create mode 100644 packages/ui/src/ui/components/DownloadAttributesButton/helpers/attributesToString.ts create mode 100644 packages/ui/src/ui/components/DownloadAttributesButton/index.ts create mode 100644 packages/ui/src/ui/pages/navigation/helpers/pathToFileName.ts diff --git a/packages/ui/src/ui/components/AttributesModal/AttributesModal.js b/packages/ui/src/ui/components/AttributesModal/AttributesModal.js index 500d7a1b2..be85c33b3 100644 --- a/packages/ui/src/ui/components/AttributesModal/AttributesModal.js +++ b/packages/ui/src/ui/components/AttributesModal/AttributesModal.js @@ -9,6 +9,8 @@ import Error from '../../components/Error/Error'; import Yson from '../Yson/Yson'; import {closeAttributesModal} from '../../store/actions/modals/attributes-modal'; +import {DownloadFileButton} from '../DownloadAttributesButton'; +import {attributesToString} from '../DownloadAttributesButton/helpers/attributesToString'; export class AttributesModal extends Component { static propTypes = { @@ -37,6 +39,19 @@ export class AttributesModal extends Component { ); } + renderDownloadButton() { + const convertedValue = attributesToString(this.props.attributes, this.props.ysonSettings); + if (!convertedValue) return null; + + return ( + + ); + } + render() { const {visible, title, loading} = this.props; @@ -44,6 +59,7 @@ export class AttributesModal extends Component { visible && ( } + footerContent={this.renderDownloadButton()} visible={visible} loading={loading} onCancel={this.handleClose} diff --git a/packages/ui/src/ui/components/DownloadAttributesButton/DownloadFileButton.tsx b/packages/ui/src/ui/components/DownloadAttributesButton/DownloadFileButton.tsx new file mode 100644 index 000000000..5aafb5304 --- /dev/null +++ b/packages/ui/src/ui/components/DownloadAttributesButton/DownloadFileButton.tsx @@ -0,0 +1,30 @@ +import React, {FC} from 'react'; +import {Button} from '@gravity-ui/uikit'; +import Icon from '../../components/Icon/Icon'; + +type Props = { + content: string; + name: string; + type?: string; +}; + +export const DownloadFileButton: FC = ({name, type, content}) => { + const handleClick = () => { + const blob = new Blob([content], {type}); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = name; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + return ( + + ); +}; diff --git a/packages/ui/src/ui/components/DownloadAttributesButton/YsonDownloadButton.tsx b/packages/ui/src/ui/components/DownloadAttributesButton/YsonDownloadButton.tsx new file mode 100644 index 000000000..cd6cfcc6f --- /dev/null +++ b/packages/ui/src/ui/components/DownloadAttributesButton/YsonDownloadButton.tsx @@ -0,0 +1,22 @@ +import React, {FC, useMemo} from 'react'; +import {UnipikaSettings} from '../Yson/StructuredYson/StructuredYsonTypes'; +import {DownloadFileButton} from './DownloadFileButton'; +import {attributesToString} from './helpers/attributesToString'; + +type Props = { + name?: string; + value?: unknown; + settings: UnipikaSettings; +}; + +export const YsonDownloadButton: FC = ({value, name, settings}) => { + const fileName = name || 'data'; + + const fileContent: string = useMemo(() => { + return attributesToString(value, settings); + }, [settings, value]); + + return ( + + ); +}; diff --git a/packages/ui/src/ui/components/DownloadAttributesButton/helpers/attributesToString.ts b/packages/ui/src/ui/components/DownloadAttributesButton/helpers/attributesToString.ts new file mode 100644 index 000000000..de23d71d3 --- /dev/null +++ b/packages/ui/src/ui/components/DownloadAttributesButton/helpers/attributesToString.ts @@ -0,0 +1,13 @@ +import unipika from '../../../common/thor/unipika'; +import {UnipikaSettings} from '../../Yson/StructuredYson/StructuredYsonTypes'; + +export const attributesToString = (attributes: unknown, settings: UnipikaSettings) => { + if (attributes === undefined || !settings) return ''; + + const convertedValue = + settings.format === 'raw-json' + ? unipika.converters.raw(attributes, settings) + : unipika.converters.yson(attributes, settings); + + return unipika.format(convertedValue, {...settings, asHTML: false}); +}; diff --git a/packages/ui/src/ui/components/DownloadAttributesButton/index.ts b/packages/ui/src/ui/components/DownloadAttributesButton/index.ts new file mode 100644 index 000000000..2e97ce659 --- /dev/null +++ b/packages/ui/src/ui/components/DownloadAttributesButton/index.ts @@ -0,0 +1,2 @@ +export {DownloadFileButton} from './DownloadFileButton'; +export {YsonDownloadButton} from './YsonDownloadButton'; diff --git a/packages/ui/src/ui/components/Modal/SimpleModal.tsx b/packages/ui/src/ui/components/Modal/SimpleModal.tsx index d6d42b1a8..a94213f51 100644 --- a/packages/ui/src/ui/components/Modal/SimpleModal.tsx +++ b/packages/ui/src/ui/components/Modal/SimpleModal.tsx @@ -19,6 +19,7 @@ interface SimpleModalProps { borderless?: boolean; onCancel: () => void; children?: React.ReactNode; + footerContent?: React.ReactNode; onOutsideClick?: () => void; className?: string; wrapperClassName?: string; @@ -74,6 +75,12 @@ class SimpleModal extends Component { return
{loading ? this.renderLoader() : children}
; } + renderFooter() { + if (!this.props.footerContent) return null; + + return
{this.props.footerContent}
; + } + render() { const {visible, onOutsideClick, size, className, wrapperClassName} = this.props; return ( @@ -82,6 +89,7 @@ class SimpleModal extends Component {
{this.renderHeader()} {this.renderContent()} + {this.renderFooter()}
) diff --git a/packages/ui/src/ui/pages/chyt/ChytPageClique/ChytPageCliqueSpeclet.tsx b/packages/ui/src/ui/pages/chyt/ChytPageClique/ChytPageCliqueSpeclet.tsx index 695673328..cfc343c21 100644 --- a/packages/ui/src/ui/pages/chyt/ChytPageClique/ChytPageCliqueSpeclet.tsx +++ b/packages/ui/src/ui/pages/chyt/ChytPageClique/ChytPageCliqueSpeclet.tsx @@ -37,6 +37,7 @@ import {YTError} from '../../../../@types/types'; import {WaitForDefaultPoolTree} from '../../../hooks/global-pool-trees'; import './ChytPageCliqueSpeclet.scss'; +import {YsonDownloadButton} from '../../../components/DownloadAttributesButton'; const block = cn('yt-chyt-clique-speclet'); @@ -103,6 +104,13 @@ function ChytSpeclet({alias, unipikaSettings}: {alias?: string; unipikaSettings: value={data} settings={unipikaSettings} folding + extraTools={ + + } /> )} diff --git a/packages/ui/src/ui/pages/components/tabs/node/NodeUnrecognizedOptions/NodeUnrecognizedOptions.tsx b/packages/ui/src/ui/pages/components/tabs/node/NodeUnrecognizedOptions/NodeUnrecognizedOptions.tsx index f0a96d499..569596f90 100644 --- a/packages/ui/src/ui/pages/components/tabs/node/NodeUnrecognizedOptions/NodeUnrecognizedOptions.tsx +++ b/packages/ui/src/ui/pages/components/tabs/node/NodeUnrecognizedOptions/NodeUnrecognizedOptions.tsx @@ -14,6 +14,7 @@ import { import Error from '../../../../../components/Error/Error'; import Yson from '../../../../../components/Yson/Yson'; import {getNodeUnrecognizedOptionsYsonSettings} from '../../../../../store/selectors/thor/unipika'; +import {YsonDownloadButton} from '../../../../../components/DownloadAttributesButton'; export function NodeUnrecognizedOptions({host}: {host: string}) { const dispatch = useDispatch(); @@ -36,6 +37,13 @@ export function NodeUnrecognizedOptions({host}: {host: string}) { settings={unipikaSettings} folding virtualized + extraTools={ + + } /> ); } diff --git a/packages/ui/src/ui/pages/job/JobGeneral/JobGeneral.tsx b/packages/ui/src/ui/pages/job/JobGeneral/JobGeneral.tsx index 14c44ea18..23acaf0dc 100644 --- a/packages/ui/src/ui/pages/job/JobGeneral/JobGeneral.tsx +++ b/packages/ui/src/ui/pages/job/JobGeneral/JobGeneral.tsx @@ -37,6 +37,7 @@ import {StaleJobIcon} from '../../../pages/operations/OperationDetail/tabs/Jobs/ import './JobGeneral.scss'; import {UI_TAB_SIZE} from '../../../constants/global'; +import {YsonDownloadButton} from '../../../components/DownloadAttributesButton'; const block = cn('job-general'); @@ -276,7 +277,18 @@ export default function JobGeneral() { - + + } + />
diff --git a/packages/ui/src/ui/pages/job/tabs/Specification/Specification.tsx b/packages/ui/src/ui/pages/job/tabs/Specification/Specification.tsx index 5cb277f2a..bee6e242f 100644 --- a/packages/ui/src/ui/pages/job/tabs/Specification/Specification.tsx +++ b/packages/ui/src/ui/pages/job/tabs/Specification/Specification.tsx @@ -3,7 +3,7 @@ import {useDispatch, useSelector} from 'react-redux'; import cn from 'bem-cn-lite'; import Yson from '../../../../components/Yson/Yson'; -import {Checkbox, Loader} from '@gravity-ui/uikit'; +import {Checkbox, Flex, Loader} from '@gravity-ui/uikit'; import ErrorBoundary from '../../../../components/ErrorBoundary/ErrorBoundary'; import LoadDataHandler from '../../../../components/LoadDataHandler/LoadDataHandler'; @@ -18,6 +18,8 @@ import { import './Specification.scss'; import {getJobSpecificationYsonSettings} from '../../../../store/selectors/thor/unipika'; +import {YsonDownloadButton} from '../../../../components/DownloadAttributesButton'; +import {getJob} from '../../../../store/selectors/job/detail'; interface SpecificationProps { jobID: string; @@ -92,6 +94,7 @@ export default function Specification({jobID}: SpecificationProps) { const {loading, loaded, error, errorData, specification} = useSelector( (state: RootState) => state.job.specification, ); + const {id} = useSelector(getJob); useEffect(() => { dispatch(loadJobSpecification(jobID)); @@ -116,7 +119,16 @@ export default function Specification({jobID}: SpecificationProps) { folding={true} settings={settings} value={specification} - extraTools={} + extraTools={ + + + + + } /> )} diff --git a/packages/ui/src/ui/pages/navigation/helpers/pathToFileName.ts b/packages/ui/src/ui/pages/navigation/helpers/pathToFileName.ts new file mode 100644 index 000000000..1febca4f4 --- /dev/null +++ b/packages/ui/src/ui/pages/navigation/helpers/pathToFileName.ts @@ -0,0 +1,3 @@ +export const pathToFileName = (path: string) => { + return path.replace(/\/+/g, '_'); +}; diff --git a/packages/ui/src/ui/pages/navigation/tabs/Attributes/Attributes.js b/packages/ui/src/ui/pages/navigation/tabs/Attributes/Attributes.js index 965223162..483e6d913 100644 --- a/packages/ui/src/ui/pages/navigation/tabs/Attributes/Attributes.js +++ b/packages/ui/src/ui/pages/navigation/tabs/Attributes/Attributes.js @@ -5,24 +5,45 @@ import {connect, useSelector} from 'react-redux'; import Yson from '../../../../components/Yson/Yson'; -import {getAttributesWithTypes, getLoadState} from '../../../../store/selectors/navigation'; +import { + getAttributesPath, + getAttributesWithTypes, + getLoadState, +} from '../../../../store/selectors/navigation'; import {RumMeasureTypes} from '../../../../rum/rum-measure-types'; import {isFinalLoadingStatus} from '../../../../utils/utils'; import {useRumMeasureStop} from '../../../../rum/RumUiContext'; import {useAppRumMeasureStart} from '../../../../rum/rum-app-measures'; +import {YsonDownloadButton} from '../../../../components/DownloadAttributesButton'; +import {pathToFileName} from '../../helpers/pathToFileName'; -function Attributes({attributes, settings}) { - return ; +function Attributes({attributes, settings, attributesPath}) { + return ( + + } + /> + ); } Attributes.propTypes = { attributes: PropTypes.object.isRequired, + attributesPath: PropTypes.string.isRequired, settings: Yson.settingsProps.isRequired, }; const mapStateToProps = (state) => ({ attributes: getAttributesWithTypes(state), settings: unipika.prepareSettings(), + attributesPath: getAttributesPath(state), }); const AttributesConnected = connect(mapStateToProps)(Attributes); diff --git a/packages/ui/src/ui/pages/navigation/tabs/Flow/PipelineSpec/PipelineSpec.tsx b/packages/ui/src/ui/pages/navigation/tabs/Flow/PipelineSpec/PipelineSpec.tsx index 5c259b763..5d39d19eb 100644 --- a/packages/ui/src/ui/pages/navigation/tabs/Flow/PipelineSpec/PipelineSpec.tsx +++ b/packages/ui/src/ui/pages/navigation/tabs/Flow/PipelineSpec/PipelineSpec.tsx @@ -33,6 +33,8 @@ import Loader from '../../../../../components/Loader/Loader'; import {UnipikaSettings} from '../../../../../components/Yson/StructuredYson/StructuredYsonTypes'; import './PipelineSpec.scss'; +import {YsonDownloadButton} from '../../../../../components/DownloadAttributesButton'; +import {pathToFileName} from '../../../helpers/pathToFileName'; const block = cn('yt-pipeline-spec'); @@ -59,6 +61,11 @@ function PipelineSpec({path, data, error, name, onSave}: PipelineSpecProps) { folding extraTools={ +