From 13734ea1c594fe685857d9841b916d3eab99d98e Mon Sep 17 00:00:00 2001 From: Aleksandr Gorodetskii <41908792+AleksandrGorodetskii@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:27:19 +0300 Subject: [PATCH] GUI Library: storage content filtering implemented (#3515) * GUI Library: storage content filtering implemented * GUI Storage content filtering: minor style adjustments * GUI Storage content filtering: use Antd datepicker instead react-daypicker, minor style adjustments * GUI Storage content filtering: minor style adjustments * GUI Storage content filtering: change size filters to mb from b, onPressEnter filter handling, change truncated results alert * GUI Storage content filtering: minor fix * GUI Storage content filtering: fix datePicker onEnter handling * GUI Storage content filtering: small fix * GUI Storage content filtering: small fix * GUI Storage content filtering: small fix * GUI Storage content filtering: change date filters from datebefore\after to from\to * GUI Storage content filtering: fix UTC string * GUI Storage content filtering: remove filters for every path. Now filters resets after any kind of navigation * GUI Storage content filtering: fix UTC format for predefined date filters * GUI Storage content filtering: minimum 3 characters name filter restriction * GUI Storage content filtering: refactoring * GUI Storage content filtering: fix default date filter * Filters fix --------- Co-authored-by: Aleksandr Gorodetskii Co-authored-by: Mikhail Rodichenko --- .../data-storage/components/filter-config.js | 40 ++ .../data-storage/components/filters.css | 44 ++ .../data-storage/components/filters.js | 454 ++++++++++++++++++ .../components/storage-pagination.js | 2 +- .../pipelines/browser/data-storage/index.js | 87 +++- .../models/dataStorage/DataStorageFilter.js | 36 ++ .../dataStorage/data-storage-listing.js | 139 ++++++ 7 files changed, 797 insertions(+), 5 deletions(-) create mode 100644 client/src/components/pipelines/browser/data-storage/components/filter-config.js create mode 100644 client/src/components/pipelines/browser/data-storage/components/filters.css create mode 100644 client/src/components/pipelines/browser/data-storage/components/filters.js create mode 100644 client/src/models/dataStorage/DataStorageFilter.js diff --git a/client/src/components/pipelines/browser/data-storage/components/filter-config.js b/client/src/components/pipelines/browser/data-storage/components/filter-config.js new file mode 100644 index 0000000000..30d523d214 --- /dev/null +++ b/client/src/components/pipelines/browser/data-storage/components/filter-config.js @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2024 EPAM Systems, Inc. (https://www.epam.com/) + * + * Licensed 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 moment from 'moment-timezone'; + +const FILTER_FIELDS = { + name: 'name', + sizeGreaterThan: 'sizeGreaterThan', + sizeLessThan: 'sizeLessThan', + dateAfter: 'dateAfter', + dateBefore: 'dateBefore', + dateFilterType: 'dateFilterType' +}; + +const PREDEFINED_DATE_FILTERS = [{ + title: 'Last week', + key: 'lastWeek', + dateAfter: (currentDate) => currentDate && moment(currentDate).subtract(7, 'days').startOf('day'), + dateBefore: undefined +}, { + title: 'Last month', + key: 'lastMonth', + dateAfter: (currentDate) => currentDate && moment(currentDate).subtract(1, 'month').endOf('day'), + dateBefore: undefined +}]; + +export {FILTER_FIELDS, PREDEFINED_DATE_FILTERS}; diff --git a/client/src/components/pipelines/browser/data-storage/components/filters.css b/client/src/components/pipelines/browser/data-storage/components/filters.css new file mode 100644 index 0000000000..c805945bf6 --- /dev/null +++ b/client/src/components/pipelines/browser/data-storage/components/filters.css @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2024 EPAM Systems, Inc. (https://www.epam.com/) + * + * Licensed 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. + */ + +.filter-wrapper { + display: flex; + flex-direction: column; + padding: 5px; + outline: none; +} + +.filter-wrapper-controls { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + margin: 5px 10px; + min-width: 180px; +} + +.input-container { + margin-bottom: 5px; + display: flex; + flex-wrap: nowrap; + align-items: center; +} + +.date-picker-container { + display: flex; + flex-wrap: nowrap; + align-items: center; + margin-bottom: 5px; +} diff --git a/client/src/components/pipelines/browser/data-storage/components/filters.js b/client/src/components/pipelines/browser/data-storage/components/filters.js new file mode 100644 index 0000000000..1bf0a9f03d --- /dev/null +++ b/client/src/components/pipelines/browser/data-storage/components/filters.js @@ -0,0 +1,454 @@ +/* + * Copyright 2017-2024 EPAM Systems, Inc. (https://www.epam.com/) + * + * Licensed 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 from 'react'; +import PropTypes from 'prop-types'; +import {observer} from 'mobx-react'; +import moment from 'moment-timezone'; +import {computed} from 'mobx'; +import { + Radio, + InputNumber, + DatePicker, + Input +} from 'antd'; +import {FILTER_FIELDS, PREDEFINED_DATE_FILTERS} from './filter-config'; +import styles from './filters.css'; + +@observer +class InputFilter extends React.Component { + static propTypes = { + storage: PropTypes.object, + hideFilterDropdown: PropTypes.func, + visible: PropTypes.bool, + label: PropTypes.string, + labelStyle: PropTypes.object, + placeholder: PropTypes.string + }; + + state = { + value: undefined + } + + componentDidMount () { + this.rebuildState(); + } + + componentDidUpdate (prevProps) { + if (this.props.storage !== prevProps.storage || (this.props.visible && !prevProps.visible)) { + this.rebuildState(); + } + } + + @computed + get storage () { + return this.props.storage; + } + + get filterKeyIsValid () { + const {filterKey} = this.props; + return filterKey && FILTER_FIELDS[filterKey]; + } + + get submitDisabled () { + const {submitDisabled} = this.props; + if (submitDisabled && typeof submitDisabled === 'function') { + return submitDisabled(this.state.value); + } + return false; + } + + rebuildState = () => { + const {filterKey} = this.props; + if (this.filterKeyIsValid && this.storage?.currentFilter) { + this.setState({value: this.storage.currentFilter[filterKey]}); + } + }; + + onChangeFilterState = event => { + this.setState({value: event.target.value}); + }; + + onApplyFilter = () => { + const {hideFilterDropdown, filterKey} = this.props; + if (!this.filterKeyIsValid || this.submitDisabled) { + return; + } + this.storage.changeFilterField( + FILTER_FIELDS[filterKey], + this.state.value + ); + hideFilterDropdown && hideFilterDropdown(); + }; + + onClearFilter = () => { + const {hideFilterDropdown, filterKey} = this.props; + if (!this.filterKeyIsValid) { + return; + } + this.storage.changeFilter({ + [FILTER_FIELDS[filterKey]]: undefined + }); + hideFilterDropdown && hideFilterDropdown(); + }; + + render () { + const { + label, + labelStyle, + placeholder + } = this.props; + return ( +
+
+ {label} + +
+ +
+ ); + } +} +@observer +class SizeFilter extends React.Component { + static propTypes = { + storage: PropTypes.object, + hideFilterDropdown: PropTypes.func, + visible: PropTypes.bool + }; + + state = { + [FILTER_FIELDS.sizeGreaterThan]: undefined, + [FILTER_FIELDS.sizeLessThan]: undefined + } + + componentDidMount () { + this.rebuildState(); + } + + componentDidUpdate (prevProps) { + if (this.props.storage !== prevProps.storage || (this.props.visible && !prevProps.visible)) { + this.rebuildState(); + } + } + + @computed + get storage () { + return this.props.storage; + } + + rebuildState = () => { + if (!this.storage?.currentFilter) { + return; + } + const from = this.storage.currentFilter[FILTER_FIELDS.sizeGreaterThan]; + const to = this.storage.currentFilter[FILTER_FIELDS.sizeLessThan]; + this.setState({ + [FILTER_FIELDS.sizeGreaterThan]: from, + [FILTER_FIELDS.sizeLessThan]: to + }); + }; + + onChangeFilterState = key => value => { + this.setState({[key]: value}); + }; + + onApplyFilter = () => { + const {hideFilterDropdown} = this.props; + const from = this.state[FILTER_FIELDS.sizeGreaterThan]; + const to = this.state[FILTER_FIELDS.sizeLessThan]; + this.storage.changeFilter({ + [FILTER_FIELDS.sizeGreaterThan]: from, + [FILTER_FIELDS.sizeLessThan]: to + }); + hideFilterDropdown && hideFilterDropdown(); + }; + + onClearFilter = () => { + const {hideFilterDropdown} = this.props; + this.storage.changeFilter({ + [FILTER_FIELDS.sizeGreaterThan]: undefined, + [FILTER_FIELDS.sizeLessThan]: undefined + }); + hideFilterDropdown && hideFilterDropdown(); + }; + + onKeyDown = (e) => { + if (e.key.toLowerCase() === 'enter') { + this.onApplyFilter(); + } + }; + + render () { + if (!this.storage) { + return null; + } + return ( +
+
+ From: + + Mb +
+
+ To: + + Mb +
+ +
+ ); + } +} + +@observer +class DateFilter extends React.Component { + static propTypes = { + storage: PropTypes.object, + hideFilterDropdown: PropTypes.func, + visible: PropTypes.bool + }; + + state = { + [FILTER_FIELDS.dateFilterType]: 'datePicker', + [FILTER_FIELDS.dateAfter]: undefined, + [FILTER_FIELDS.dateBefore]: undefined + } + + containerRef; + + componentDidMount () { + this.rebuildState(); + } + + componentDidUpdate (prevProps) { + if (this.props.storage !== prevProps.storage || (this.props.visible && !prevProps.visible)) { + this.rebuildState(); + } + } + + @computed + get storage () { + return this.props.storage; + } + + get predefinedDateFilters () { + return PREDEFINED_DATE_FILTERS.map((filter) => ({ + title: filter.title, + key: filter.key, + dateAfter: filter.dateAfter || undefined, + dateBefore: filter.dateBefore || undefined + })); + } + + rebuildState = () => { + if (!this.storage?.currentFilter) { + return; + } + const dateFilterType = this.storage.currentFilter[FILTER_FIELDS.dateFilterType] || 'datePicker'; + const from = this.storage.currentFilter[FILTER_FIELDS.dateAfter]; + const to = this.storage.currentFilter[FILTER_FIELDS.dateBefore]; + this.setState({ + [FILTER_FIELDS.dateFilterType]: dateFilterType, + [FILTER_FIELDS.dateAfter]: from, + [FILTER_FIELDS.dateBefore]: to + }); + }; + + onApplyFilter = () => { + const {hideFilterDropdown} = this.props; + const dateFilterType = this.state[FILTER_FIELDS.dateFilterType]; + const from = this.state[FILTER_FIELDS.dateAfter]; + const to = this.state[FILTER_FIELDS.dateBefore]; + this.storage.changeFilter({ + [FILTER_FIELDS.dateFilterType]: dateFilterType, + [FILTER_FIELDS.dateAfter]: from, + [FILTER_FIELDS.dateBefore]: to + }); + hideFilterDropdown && hideFilterDropdown(); + }; + + onClearFilter = () => { + const {hideFilterDropdown} = this.props; + this.storage.changeFilter({ + [FILTER_FIELDS.dateFilterType]: 'datePicker', + [FILTER_FIELDS.dateAfter]: undefined, + [FILTER_FIELDS.dateBefore]: undefined + }); + hideFilterDropdown && hideFilterDropdown(); + }; + + onChangeRadio = (event) => { + this.setState({dateFilterType: event.target.value}, () => { + const type = event.target.value; + const predefined = PREDEFINED_DATE_FILTERS.find(({key}) => key === type); + const currentDate = moment(); + const from = typeof predefined?.dateAfter === 'function' + ? predefined.dateAfter(currentDate) + : undefined; + const to = typeof predefined?.dateBefore === 'function' + ? predefined.dateBefore(currentDate) + : undefined; + this.setState({ + [FILTER_FIELDS.dateAfter]: from, + [FILTER_FIELDS.dateBefore]: to + }); + }); + }; + + onChangeFrom = (date) => { + let dateString; + if (date) { + dateString = moment(date).startOf('d'); + } + this.setState({[FILTER_FIELDS.dateAfter]: dateString}); + }; + + onChangeTo = (date) => { + let dateString; + if (date) { + dateString = moment(date).endOf('d'); + } + this.setState({[FILTER_FIELDS.dateBefore]: dateString}); + }; + + onKeyDown = event => { + if (event.key.toLowerCase() === 'enter') { + this.onApplyFilter(); + } + } + + onOpenChange = (visible) => { + if (!visible) { + this.containerRef && this.containerRef.focus(); + } + }; + + renderPicker = () => { + const {dateFilterType} = this.state; + if (dateFilterType !== 'datePicker') { + return null; + } + return ( +
+
+ From: + node.parentNode} + onChange={this.onChangeFrom} + value={this.state[FILTER_FIELDS.dateAfter]} + onOpenChange={this.onOpenChange} + /> +
+
+ To: + node.parentNode} + onChange={this.onChangeTo} + onOpenChange={this.onOpenChange} + value={this.state[FILTER_FIELDS.dateBefore]} + /> +
+
+ ); + }; + + render () { + if (!this.storage) { + return null; + } + return ( +
{ + this.containerRef = el; + }} + onKeyDown={this.onKeyDown} + className={styles.filterWrapper} + tabIndex={-1} + > + + {this.predefinedDateFilters.map(filter => ( + {filter.title} + ))} + + Custom + + + {this.renderPicker()} + +
+ ); + } +} + +function FilterFooter ({onOk, onClear, okDisabled}) { + const handleOk = () => !okDisabled && onOk && onOk(); + const handleClear = () => onClear && onClear(); + return ( +
+ + OK + + Clear +
+ ); +} + +FilterFooter.propTypes = { + onOk: PropTypes.func, + onClear: PropTypes.func, + okDisabled: PropTypes.bool +}; + +export {SizeFilter, DateFilter, InputFilter, FILTER_FIELDS}; diff --git a/client/src/components/pipelines/browser/data-storage/components/storage-pagination.js b/client/src/components/pipelines/browser/data-storage/components/storage-pagination.js index d63b59e8b4..cbd5b722e4 100644 --- a/client/src/components/pipelines/browser/data-storage/components/storage-pagination.js +++ b/client/src/components/pipelines/browser/data-storage/components/storage-pagination.js @@ -21,7 +21,7 @@ import classNames from 'classnames'; import styles from './storage-pagination.css'; function StoragePagination ({className, style, storage}) { - if (!storage || !storage.infoLoaded) { + if (!storage || !storage.infoLoaded || storage.filtersApplied) { return null; } return ( diff --git a/client/src/components/pipelines/browser/data-storage/index.js b/client/src/components/pipelines/browser/data-storage/index.js index 8e6abb2afe..2d41002eff 100644 --- a/client/src/components/pipelines/browser/data-storage/index.js +++ b/client/src/components/pipelines/browser/data-storage/index.js @@ -86,6 +86,7 @@ import { METADATA_KEY as REQUEST_DAV_ACCESS_ATTRIBUTE } from '../../../special/metadata/special/request-dav-access'; import StorageSize from '../../../special/storage-size'; +import highlightText from '../../../special/highlightText'; import {extractFileShareMountList} from '../forms/DataStoragePathInput'; import SharedItemInfo from '../forms/data-storage-item-sharing/SharedItemInfo'; import {SAMPLE_SHEET_FILE_NAME_REGEXP} from '../../../special/sample-sheet/utilities'; @@ -101,6 +102,7 @@ import StorageSharedLinkButton from './components/storage-shared-link-button'; import DownloadFileButton from './components/download-file-button'; import handleDownloadItems from '../../../special/download-storage-items'; import styles from '../Browser.css'; +import {SizeFilter, DateFilter, InputFilter, FILTER_FIELDS} from './components/filters'; const STORAGE_CLASSES = { standard: 'STANDARD', @@ -177,6 +179,7 @@ export default class DataStorage extends React.Component { }); @observable generateDownloadUrls; + @observable filterDropdownVisible; get showMetadata () { if (this.state.metadata === undefined && this.storage.info) { @@ -1476,9 +1479,18 @@ export default class DataStorage extends React.Component { ); }; + const filteredStatus = (keys = []) => { + const filtered = keys.some(key => !!this.storage.currentFilter?.[key]); + return { + filtered, + filteredValue: filtered ? ['filtered'] : null + }; + }; + const hideFilterDropdown = () => { + this.filterDropdownVisible = undefined; + }; const selectionColumn = { key: 'selection', - title: '', className: (this.showVersions || hasVersions) ? styles.checkboxCellVersions : styles.checkboxCell, @@ -1588,11 +1600,30 @@ export default class DataStorage extends React.Component { title: 'Name', className: styles.nameCell, render: (text, item) => { + const search = this.storage.currentFilter[FILTER_FIELDS.name]; + const highlightedText = this.storage.filtersApplied && search + ? highlightText(text, search) + : text; if (item.latest) { - return `${text} (latest)`; + return `${highlightedText} (latest)`; } - return text; + return highlightedText; + }, + filterDropdown: ( + (value || '').length < 3} + /> + ), + filterDropdownVisible: this.filterDropdownVisible === 'name', + onFilterDropdownVisibleChange: (visible) => { + this.filterDropdownVisible = visible ? 'name' : undefined; }, + ...filteredStatus([FILTER_FIELDS.name]), onCellClick: (item) => this.didSelectDataStorageItem(item) }; const sizeColumn = { @@ -1601,6 +1632,18 @@ export default class DataStorage extends React.Component { title: 'Size', className: styles.sizeCell, render: size => displaySize(size), + filterDropdown: ( + + ), + filterDropdownVisible: this.filterDropdownVisible === 'size', + onFilterDropdownVisibleChange: (visible) => { + this.filterDropdownVisible = visible ? 'size' : undefined; + }, + ...filteredStatus([FILTER_FIELDS.sizeGreaterThan, FILTER_FIELDS.sizeLessThan]), onCellClick: (item) => this.didSelectDataStorageItem(item) }; const changedColumn = { @@ -1609,6 +1652,18 @@ export default class DataStorage extends React.Component { title: 'Date changed', className: styles.changedCell, render: (date) => date ? displayDate(date) : '', + filterDropdown: ( + + ), + filterDropdownVisible: this.filterDropdownVisible === 'date', + onFilterDropdownVisibleChange: (visible) => { + this.filterDropdownVisible = visible ? 'date' : undefined; + }, + ...filteredStatus([FILTER_FIELDS.dateAfter, FILTER_FIELDS.dateBefore]), onCellClick: (item) => this.didSelectDataStorageItem(item) }; const labelsColumn = { @@ -1627,6 +1682,19 @@ export default class DataStorage extends React.Component { const actionsColumn = { key: 'actions', className: styles.itemActions, + title: ( +
+ {this.storage.resultsFiltered ? ( + + ) : null} +
+ ), render: this.actionsRenderer }; @@ -2534,6 +2602,14 @@ export default class DataStorage extends React.Component { navigate={this.navigate} navigateFull={this.navigateFull} /> + {this.storage.resultsFilteredAndTruncated ? ( + + ) : null} { this.renderContent() } @@ -2833,12 +2909,15 @@ export default class DataStorage extends React.Component { } updateStorageIfRequired = () => { - this.storage.initialize( + const changed = this.storage.initialize( this.props.storageId, this.props.path, this.props.showVersions, this.props.showArchives ); + if (changed) { + this.storage.resetFilter(); + } }; clearSelectedItemsIfRequired = () => { diff --git a/client/src/models/dataStorage/DataStorageFilter.js b/client/src/models/dataStorage/DataStorageFilter.js new file mode 100644 index 0000000000..35635bac31 --- /dev/null +++ b/client/src/models/dataStorage/DataStorageFilter.js @@ -0,0 +1,36 @@ +/* + * Copyright 2017-2024 EPAM Systems, Inc. (https://www.epam.com/) + * + * Licensed 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 RemotePost from '../basic/RemotePost'; + +export default class DataStorageFilter extends RemotePost { + constructor ( + id, + path, + showVersion = false, + showArchives = false + ) { + super(); + const query = [ + !!path && `path=${encodeURIComponent(path)}`, + `showVersion=${!!showVersion}`, + `showArchived=${!!showArchives}` + ] + .filter(Boolean) + .join('&'); + this.url = `/datastorage/${id}/list/filter?${query}`; + } +} diff --git a/client/src/models/dataStorage/data-storage-listing.js b/client/src/models/dataStorage/data-storage-listing.js index 8dc612764b..97209a4e8e 100644 --- a/client/src/models/dataStorage/data-storage-listing.js +++ b/client/src/models/dataStorage/data-storage-listing.js @@ -15,16 +15,25 @@ */ import {action, computed, observable} from 'mobx'; +import moment from 'moment-timezone'; import dataStorages from './DataStorages'; import preferences from '../preferences/PreferencesLoad'; import authenticatedUserInfo from '../user/WhoAmI'; import DataStorageRequest from './DataStoragePage'; +import DataStorageFilter from './DataStorageFilter'; import roleModel from '../../utils/roleModel'; import MetadataLoad from '../metadata/MetadataLoad'; const DEFAULT_DELIMITER = '/'; const PAGE_SIZE = 40; +const mbToBytes = mb => { + if (isNaN(mb)) { + return; + } + return Math.round(mb * (1024 ** 2)); +}; + /** * Returns true if user is allowed to download from storage according to the * `download.enabled` attribute value @@ -268,6 +277,21 @@ class DataStorageListing { @observable pagePath; @observable downloadEnabled = false; + /** + * Filters info. + * Request results may be truncated. + */ + @observable filters = { + name: undefined, + sizeGreaterThan: undefined, + sizeLessThan: undefined, + dateFilterType: undefined, + dateAfter: undefined, + dateBefore: undefined + }; + @observable resultsTruncated = false; + @observable filtersApplied = false; + /** * @param {DataStoragePagesOptions} options */ @@ -363,6 +387,30 @@ class DataStorageListing { }; } + @computed + get currentFilter () { + return this.filters; + } + + @computed + get filtersEmpty () { + if (!this.currentFilter) { + return true; + } + return Object.values(this.currentFilter) + .every(value => value === undefined); + } + + @computed + get resultsFiltered () { + return this.filtersApplied && !this.filtersEmpty; + } + + @computed + get resultsFilteredAndTruncated () { + return this.resultsFiltered && this.resultsTruncated; + } + _increaseUniqueToken = () => { this.token = (this.token || 0) + 1; return this.token; @@ -387,6 +435,21 @@ class DataStorageListing { this.markers = resetMarkersForPath(); }; + @action + resetFilter = (silent = true) => { + this.filters = { + name: undefined, + sizeGreaterThan: undefined, + sizeLessThan: undefined, + dateFilterType: undefined, + dateAfter: undefined, + dateBefore: undefined + }; + if (!silent) { + this.refreshCurrentPath(true); + } + }; + @action initialize = ( storageId, @@ -531,6 +594,80 @@ class DataStorageListing { return true; }; + @action + changeFilterField = (key, value, applyChanges = true) => { + this.currentFilter[key] = value; + if (applyChanges) { + this.applyFilters(); + } + }; + + @action + changeFilter = (newFilterObj = {}, applyChanges = true) => { + Object.keys(newFilterObj).forEach(key => { + this.changeFilterField(key, newFilterObj[key], false); + }); + if (applyChanges) { + this.applyFilters(); + } + }; + + @action + applyFilters = async () => { + const pathCorrected = correctPath( + this.path, + { + leadingSlash: false, + trailingSlash: false, + undefinedAsEmpty: true + } + ); + const formatToUTCString = date => date + ? moment.utc(date).format('YYYY-MM-DD HH:mm:ss.SSS') + : undefined; + try { + const request = new DataStorageFilter( + this.storageId, + pathCorrected ? decodeURIComponent(pathCorrected) : undefined + ); + let payload = { + nameFilter: this.currentFilter?.name, + sizeGreaterThan: mbToBytes(this.currentFilter?.sizeGreaterThan), + sizeLessThan: mbToBytes(this.currentFilter?.sizeLessThan), + dateAfter: formatToUTCString(this.currentFilter?.dateAfter), + dateBefore: formatToUTCString(this.currentFilter?.dateBefore) + }; + payload = Object.fromEntries(Object.entries(payload) + .filter(([_, value]) => value !== undefined) + ); + if (!Object.keys(payload).length) { + return this.refreshCurrentPath(true); + } + await request.send(payload); + if (request.error) { + throw new Error(request.error); + } + if (!request.loaded) { + throw new Error('Error loading page'); + } + const {results = [], nextPageMarker} = request.value || {}; + this.resultsTruncated = !!nextPageMarker; + this.filtersApplied = true; + this.pageElements = results; + this.pageLoaded = true; + this.pagePath = pathCorrected; + } catch (error) { + this.pageElements = []; + this.pageError = error.message; + this.pageLoaded = false; + this.pagePath = pathCorrected; + } finally { + this.pagePending = false; + this.pageLoaded = !this.pageError; + this.filtersApplied = true; + } + }; + @action fetchCurrentPage = async () => { const token = this._increaseUniqueToken(); @@ -575,6 +712,7 @@ class DataStorageListing { this.pageLoaded = true; this.pagePath = pathCorrected; this.markers = insertNextPageMarker(this.path, nextPageMarker, this.markers); + this.filtersApplied = false; }); } catch (error) { submitChanges(() => { @@ -587,6 +725,7 @@ class DataStorageListing { submitChanges(() => { this.pagePending = false; this.pageLoaded = !this.pageError; + this.filtersApplied = false; }); } };