From 922baddb4f39bc85fe8df363df2ce88be445323a Mon Sep 17 00:00:00 2001 From: Samantha Date: Wed, 29 Nov 2023 16:09:40 -0500 Subject: [PATCH] [ALS-5163] Add UI Pagination to Normal and Advanced workspace view (#144) - Change filter behavior by removing it from the environments store and adding it to the Nomral view. - Add pagination component. - Add items per page preference to advanced view model. --- .../environments-sc/ScEnvironmentsStore.js | 23 +-- .../environments-sc/advanced/ScEnvView.js | 22 +- .../environments-sc/ScEnvironmentsList.js | 195 +++++++++++------- .../advanced/ScEnvAdvancedList.js | 88 ++++++-- .../src/parts/helpers/Paginate.js | 69 +++++++ 5 files changed, 272 insertions(+), 125 deletions(-) create mode 100644 addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/helpers/Paginate.js diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironmentsStore.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironmentsStore.js index b1fbf88e61..f8637e77f7 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironmentsStore.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/ScEnvironmentsStore.js @@ -44,21 +44,6 @@ const filterNames = { TERMINATED: 'terminated', }; -// A map, with the key being the filter name and the value being the function that will be used to filter the workspace -const filters = { - [filterNames.ALL]: () => true, - [filterNames.AVAILABLE]: env => env.status === 'COMPLETED' || env.status === 'TAINTED', - [filterNames.STOPPED]: env => env.status === 'STOPPED', - [filterNames.PENDING]: env => - env.status === 'PENDING' || env.status === 'TERMINATING' || env.status === 'STARTING' || env.status === 'STOPPING', - [filterNames.ERRORED]: env => - env.status === 'FAILED' || - env.status === 'TERMINATING_FAILED' || - env.status === 'STARTING_FAILED' || - env.status === 'STOPPING_FAILED', - [filterNames.TERMINATED]: env => env.status === 'TERMINATED', -}; - // ================================================================== // ScEnvironmentsStore // ================================================================== @@ -202,13 +187,7 @@ const ScEnvironmentsStore = BaseStore.named('ScEnvironmentsStore') }, get list() { - return _.orderBy(values(self.environments), ['createdAt', 'name'], ['desc', 'asc']); - }, - - filtered(filterName) { - const filter = filters[filterName] || (() => true); - const filtered = _.filter(values(self.environments), filter); - return _.orderBy(filtered, ['createdAt', 'name'], ['desc', 'asc']); + return values(self.environments); }, getScEnvironment(id) { diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/advanced/ScEnvView.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/advanced/ScEnvView.js index 6f33540be8..74ade8474e 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/advanced/ScEnvView.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/models/environments-sc/advanced/ScEnvView.js @@ -30,17 +30,23 @@ const Sort = types.model({ order: types.enumeration('order', [ORDER.DESC, ORDER.ASC]), }); +const DEFAULT_LIMIT_PER_PAGE = 25; + const ScEnvView = types .model('ScEnvView', { - activeFilters: types.optional(types.array(Filter), []), - activeMode: types.optional(types.string, 'and'), + filters: types.optional(types.array(Filter), []), + mode: types.optional(types.string, 'and'), sort: types.optional(Sort, { key: 'createdAt', order: ORDER.DESC }), view: types.optional(types.enumeration('view', [VIEW.ADVANCED, VIEW.NORMAL]), VIEW.NORMAL), + perPage: DEFAULT_LIMIT_PER_PAGE, }) .actions(self => ({ setFilters(filters = [], mode = 'or') { - self.activeFilters = cloneDeep(filters); - self.activeMode = mode; + self.filters = cloneDeep(filters); + self.mode = mode; + }, + setPerPage(num) { + self.perPage = num; }, setSort(key) { if (key === self.sort.key) { @@ -53,14 +59,6 @@ const ScEnvView = types toggleView() { self.view = self.view === VIEW.ADVANCED ? VIEW.NORMAL : VIEW.ADVANCED; }, - })) - .views(self => ({ - get filters() { - return self.activeFilters; - }, - get mode() { - return self.activeMode; - }, })); function registerContextItems(appContext) { diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js index bd90e2b4d6..2041f15216 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/ScEnvironmentsList.js @@ -17,11 +17,10 @@ import _ from 'lodash'; import { decorate, computed, action, observable, runInAction } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; -import { Container, Segment, Header, Icon, Form, Grid, Input, Dropdown } from 'semantic-ui-react'; +import { Container, Segment, Header, Icon, Grid, Input, Dropdown } from 'semantic-ui-react'; import { gotoFn } from '@aws-ee/base-ui/dist/helpers/routing'; -import { swallowError, storage } from '@aws-ee/base-ui/dist/helpers/utils'; -import storageKeys from '@aws-ee/base-ui/dist/models/constants/local-storage-keys'; +import { swallowError } from '@aws-ee/base-ui/dist/helpers/utils'; import { isStoreLoading, isStoreEmpty, @@ -32,10 +31,11 @@ import { import ErrorBox from '@aws-ee/base-ui/dist/parts/helpers/ErrorBox'; import ProgressPlaceHolder from '@aws-ee/base-ui/dist/parts/helpers/BasicProgressPlaceholder'; -import { filterNames } from '../../models/environments-sc/ScEnvironmentsStore'; import ScEnvironmentCard from './ScEnvironmentCard'; import ScEnvironmentsFilterButtons from './parts/ScEnvironmentsFilterButtons'; import EnvsHeader from './ScEnvsHeader'; +import Paginate from '../helpers/Paginate'; +import { filterNames } from '../../models/environments-sc/ScEnvironmentsStore'; const envOptions = [ { key: 'any', text: 'Any Attribute', value: 'any' }, @@ -55,13 +55,11 @@ class ScEnvironmentsList extends React.Component { constructor(props) { super(props); runInAction(() => { - const key = storageKeys.workspacesFilterName; - const name = storage.getItem(key) || filterNames.ALL; - storage.setItem(key, name); - this.selectedFilter = name; + this.statusFilter = filterNames.ALL; this.provisionDisabled = false; this.searchType = 'any'; this.search = ''; + this.page = 1; }); } @@ -121,21 +119,38 @@ class ScEnvironmentsList extends React.Component { goto(`/workspaces/create`); }; - handleSelectedFilter = name => { - this.selectedFilter = name; - const key = storageKeys.workspacesFilterName; - storage.setItem(key, name); - }; - handleViewToggle() { return () => runInAction(() => this.viewStore.toggleView()); } + handleSearchAndFilter({ search, searchType, status }) { + runInAction(() => { + this.page = 1; // Reset page number on search/filter change + this.search = search || this.search; + this.searchType = searchType || this.searchType; + this.statusFilter = status || this.statusFilter; + }); + } + + handlePaginationChange() { + return (number) => runInAction(() => { + this.page = number; + window.scrollTo(0, 0); + }); + } + + handlePerPageChange() { + return (number) => runInAction(() => { + this.page = 1; + this.viewStore.setPerPage(number); + }); + } + render() { const store = this.envsStore; let content = null; - let list = []; let total = 0; + let current = 0; const projects = this.getProjects(); const appStreamProjectIds = _.map( @@ -154,16 +169,17 @@ class ScEnvironmentsList extends React.Component { } else if (isStoreEmpty(store)) { content = this.renderEmpty(); } else if (isStoreNotEmpty(store)) { - list = this.searchAndFilter(); - content = this.renderMain(list); + const { paginatedEnvList, filteredEnvsCount } = this.getPaginatedEnvs(); + current = filteredEnvsCount; total = store.total; + content = this.renderMain(paginatedEnvList, filteredEnvsCount); } return ( {this.provisionDisabled && this.renderMissingAppStreamConfig()} searchFilter(env) && statusFilter(env)); + const orderedEnvs = _.orderBy(filteredEnvs, ['createdAt', 'name'], ['desc', 'asc']); + + const firstIndex = (this.page - 1) * this.viewStore.perPage; + const lastIndex = Math.min(this.page * this.viewStore.perPage, orderedEnvs.length); + return { + paginatedEnvList: orderedEnvs.slice(firstIndex, lastIndex), + filteredEnvsCount: filteredEnvs.length + }; + } + + statusFilterMethod() { + const matchStatus = (...A) => env => _.find(A, a => a.includes(env.status)); + const filterMap = { + [filterNames.ALL]: () => true, + [filterNames.AVAILABLE]: matchStatus('COMPLETED', 'TAINTED'), + [filterNames.STOPPED]: matchStatus('STOPPED'), + [filterNames.PENDING]: matchStatus('PENDING', 'TERMINATING', 'STARTING', 'STOPPING'), + [filterNames.ERRORED]: matchStatus('FAILED', 'TERMINATING_FAILED', 'STARTING_FAILED', 'STOPPING_FAILED'), + [filterNames.TERMINATED]: matchStatus('TERMINATED'), + }; + return filterMap[this.statusFilter]; + } + + searchFilterMethod() { + if (!this.search) { + return () => true; + } + const searchString = `(${_.escapeRegExp(this.search).replace(' or ', '|')})`; const exp = new RegExp(searchString, 'i'); @@ -226,61 +271,60 @@ class ScEnvironmentsList extends React.Component { configType: env => exp.test(configName(env)), study: env => exp.test(env.studyIds.join(', ')), }; - - return _.filter(list, searchMap[this.searchType]); + return searchMap[this.searchType]; } - renderMain(list) { - const isEmpty = _.isEmpty(list); + renderMain(paginatedEnvList, filteredEnvsCount) { + const isEmpty = _.isEmpty(paginatedEnvList); + const lastIndex = paginatedEnvList.length - 1; return (
-
- - - - - runInAction(() => { - this.searchType = data.value; - }) - } - /> - } - placeholder="Search" - icon="search" - iconPosition="left" - onChange={(e, data) => - runInAction(() => { - this.search = data.value; - }) - } - /> - - - - - - -
- {!isEmpty && - _.map(list, item => ( - - + + + + this.handleSearchAndFilter({ searchType: data.value }, e)} + /> + } + placeholder="Search" + icon="search" + iconPosition="left" + onChange={_.debounce((e, data) => this.handleSearchAndFilter({ search: data.value }, e), 500)} + /> + + + this.handleSearchAndFilter({ status: name })} + /> + + + + + {!isEmpty && _.map(paginatedEnvList, (env, index) => ( + + ))} - {isEmpty && ( + {isEmpty && (
@@ -289,6 +333,7 @@ class ScEnvironmentsList extends React.Component {
)} +
); } @@ -308,16 +353,22 @@ class ScEnvironmentsList extends React.Component { // see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da decorate(ScEnvironmentsList, { - selectedFilter: observable, - provisionDisabled: observable, envsStore: computed, envTypesStore: computed, viewStore: computed, isAdmin: computed, + handleCreateEnvironment: action, - handleSelectedFilter: action, handleViewToggle: action, + handlePaginationChange: action, + handlePerPageChange: action, + handleSearchAndFilter: action, + + provisionDisabled: observable, + statusFilter: observable, search: observable, + searchType: observable, + page: observable, }); export default inject( diff --git a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/advanced/ScEnvAdvancedList.js b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/advanced/ScEnvAdvancedList.js index b808d3fb97..38f7f3784f 100644 --- a/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/advanced/ScEnvAdvancedList.js +++ b/addons/addon-base-raas-ui/packages/base-raas-ui/src/parts/environments-sc/advanced/ScEnvAdvancedList.js @@ -1,6 +1,6 @@ import React from 'react'; import _ from 'lodash'; -import { decorate, computed, action, runInAction } from 'mobx'; +import { decorate, computed, action, observable, runInAction } from 'mobx'; import { observer, inject } from 'mobx-react'; import { withRouter } from 'react-router-dom'; import { Container, Icon, Segment, Header } from 'semantic-ui-react'; @@ -21,6 +21,7 @@ import CompactTable from './CompactTable'; import FilterBox from './FilterBox'; import ActionButtons from './ActionButtons'; import EnvsHeader from '../ScEnvsHeader'; +import Paginate from '../../helpers/Paginate'; const statusMap = [ { name: 'AVAILABLE', list: ['COMPLETED', 'TAINTED'] }, @@ -35,6 +36,21 @@ class ScEnvAdvancedList extends React.Component { super(props); runInAction(() => { this.goto = gotoFn(this); + this.page = 1; + }); + } + + handlePaginationChange() { + return (number) => runInAction(() => { + this.page = number; + window.scrollTo(0, 0); + }); + } + + handlePerPageChange() { + return (number) => runInAction(() => { + this.page = 1; + this.viewStore.setPerPage(number); }); } @@ -114,8 +130,8 @@ class ScEnvAdvancedList extends React.Component { return fields; } - getEnvs(envs, filters, sort) { - let envRow = _(envs).map(env => ({ + getFilteredEnvs() { + let envRows = _(this.envsStore.list).map(env => ({ id: env.id, name: env.name, user: this.userName(env), @@ -141,22 +157,33 @@ class ScEnvAdvancedList extends React.Component { ), })); - if (filters.length > 0) { + if (this.viewStore.filters.length > 0) { const or = (list, predicate) => _.find(list, predicate); const and = (list, predicate) => _.reduce(list, (acc, value) => acc && predicate(value), true); const listMatcher = (A, b) => _.find(A, a => a === b); const regexMatcher = (a, b) => new RegExp(_.escapeRegExp(a), 'i').test(b); - envRow = envRow.filter(env => { + envRows = envRows.filter(env => { const operator = this.viewStore.mode === 'or' ? or : and; - return operator(filters, ({ key, value, match }) => { + return operator(this.viewStore.filters, ({ key, value, match }) => { const matcher = match === 'partial' ? regexMatcher : listMatcher; return matcher(value, env[key]); }); }); } - return envRow.orderBy(sort.key, sort.order).value(); + return envRows.orderBy(this.viewStore.sort.key, this.viewStore.sort.order).value(); + } + + getPaginatedEnvs() { + const filteredEnvs = this.getFilteredEnvs(); + + const firstIndex = (this.page - 1) * this.viewStore.perPage; + const lastIndex = Math.min(this.page * this.viewStore.perPage, filteredEnvs.length); + return { + paginatedEnvList: filteredEnvs.slice(firstIndex, lastIndex), + filteredEnvsCount: filteredEnvs.length + }; } handleAction() { @@ -171,7 +198,10 @@ class ScEnvAdvancedList extends React.Component { } handleFilter() { - return ({ filters, mode }) => runInAction(() => this.viewStore.setFilters(filters, mode)); + return ({ filters, mode }) => runInAction(() => { + this.page = 1; + this.viewStore.setFilters(filters, mode) + }); } handleSort() { @@ -192,15 +222,16 @@ class ScEnvAdvancedList extends React.Component { } render() { + const store = this.envsStore; let content = null; - let list = []; let total = 0; + let current = 0; - if (isStoreError(this.envsStore)) { - content = ; - } else if (isStoreLoading(this.envsStore)) { + if (isStoreError(store)) { + content = ; + } else if (isStoreLoading(store)) { content = ; - } else if (isStoreEmpty(this.envsStore)) { + } else if (isStoreEmpty(store)) { content = (
@@ -210,11 +241,12 @@ class ScEnvAdvancedList extends React.Component {
); - } else if (isStoreNotEmpty(this.envsStore)) { - const fields = this.getEnvFields(this.envsStore.list); + } else if (isStoreNotEmpty(store)) { + const fields = this.getEnvFields(store.list); const tableColumns = fields.map(column => _.pick(column, ['key', 'label', 'sortable', 'type'])); - list = this.getEnvs(this.envsStore.list, this.viewStore.filters, this.viewStore.sort); - total = this.envsStore.total; + const { paginatedEnvList, filteredEnvsCount } = this.getPaginatedEnvs(); + current = filteredEnvsCount; + total = store.total; content = ( <> @@ -224,7 +256,20 @@ class ScEnvAdvancedList extends React.Component { fields={fields} onFilter={this.handleFilter()} /> - + + + ); } @@ -232,7 +277,7 @@ class ScEnvAdvancedList extends React.Component { return ( { }, + onPerPageChange = () => { }, + children +}) => { + const totalPages = Math.ceil(totalEntries / entriesPerPage); + const perPageOptions = [5, 10, 25, 50].map(count => ({ value: count, text: `${count} Items per page` })); + + function handlePageChange(number) { + return () => onPageChange(number); + } + + function handlePerPageChange(_, { value }) { + onPerPageChange(value); + } + + return ( + <> + {children} +
+ + {totalEntries > entriesPerPage && ( + <> + + ))} + + +
+ + ); +}; + +export default Paginate;