Skip to content

Commit 8e4fef3

Browse files
authored
feat: add pagination to Dashboard page (#3403)
* feat: created new dashboard paginated files * feat: adding function to render dashboards * chore: clean up * feat: pagination is working * chore: moved filter one component higher * chore: clean up * feat: adding a spinner to dashboardsIndexPaginated * feat: usibility fixes with url * feat: feature flag added to ui * chore: clean up * fix: prettier * chore: clean up * fix: prettier * fix: fixing pagination controls position * fix: prettier
1 parent 1aa5187 commit 8e4fef3

File tree

5 files changed

+597
-5
lines changed

5 files changed

+597
-5
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Libraries
2+
import React, {PureComponent} from 'react'
3+
import {connect} from 'react-redux'
4+
5+
// Components
6+
import DashboardCard from 'src/dashboards/components/dashboard_index/DashboardCard'
7+
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
8+
9+
// Types
10+
import {AppState, Dashboard} from 'src/types'
11+
import {LimitStatus} from 'src/cloud/actions/limits'
12+
13+
// Utils
14+
import {extractDashboardLimits} from 'src/cloud/utils/limits'
15+
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
16+
import {CLOUD} from 'src/shared/constants'
17+
18+
let getPinnedItems
19+
if (CLOUD) {
20+
getPinnedItems = require('src/shared/contexts/pinneditems').getPinnedItems
21+
}
22+
23+
interface StateProps {
24+
limitStatus: LimitStatus['status']
25+
}
26+
27+
interface OwnProps {
28+
dashboards: Dashboard[]
29+
onFilterChange: (searchTerm: string) => void
30+
}
31+
32+
class DashboardCards extends PureComponent<OwnProps & StateProps> {
33+
private _isMounted = true
34+
35+
state = {
36+
pinnedItems: [],
37+
}
38+
39+
public componentDidMount() {
40+
if (isFlagEnabled('pinnedItems') && CLOUD) {
41+
getPinnedItems()
42+
.then(res => {
43+
if (this._isMounted) {
44+
this.setState(prev => ({...prev, pinnedItems: res}))
45+
}
46+
})
47+
.catch(err => {
48+
console.error(err)
49+
})
50+
}
51+
}
52+
53+
public componentWillUnmount() {
54+
this._isMounted = false
55+
}
56+
57+
public render() {
58+
const {dashboards, onFilterChange} = this.props
59+
60+
const {pinnedItems} = this.state
61+
62+
return (
63+
<div>
64+
<div className="dashboards-card-grid">
65+
{dashboards.map(({id, name, description, labels, meta}) => (
66+
<DashboardCard
67+
key={id}
68+
id={id}
69+
name={name}
70+
labels={labels}
71+
updatedAt={meta.updatedAt}
72+
description={description}
73+
onFilterChange={onFilterChange}
74+
isPinned={
75+
!!pinnedItems.find(item => item?.metadata.dashboardID === id)
76+
}
77+
/>
78+
))}
79+
<AssetLimitAlert
80+
className="dashboards--asset-alert"
81+
resourceName="dashboards"
82+
limitStatus={this.props.limitStatus}
83+
/>
84+
</div>
85+
</div>
86+
)
87+
}
88+
}
89+
90+
const mstp = (state: AppState) => {
91+
return {
92+
limitStatus: extractDashboardLimits(state),
93+
}
94+
}
95+
96+
export default connect(mstp)(DashboardCards)
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Libraries
2+
import React, {Component, createRef, RefObject} from 'react'
3+
import {connect, ConnectedProps} from 'react-redux'
4+
import {withRouter, RouteComponentProps} from 'react-router-dom'
5+
import memoizeOne from 'memoize-one'
6+
7+
// Components
8+
import DashboardsTableEmpty from 'src/dashboards/components/dashboard_index/DashboardsTableEmpty'
9+
import DashboardCardsPaginated from 'src/dashboards/components/dashboard_index/DashboardCardsPaginated'
10+
import {ResourceList} from '@influxdata/clockface'
11+
12+
// Actions
13+
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/ranges'
14+
import {checkDashboardLimits as checkDashboardLimitsAction} from 'src/cloud/actions/limits'
15+
import {createDashboard} from 'src/dashboards/actions/thunks'
16+
17+
// Decorators
18+
import {ErrorHandling} from 'src/shared/decorators/errors'
19+
20+
// Types
21+
import {Dashboard, AppState, Pageable, RemoteDataState} from 'src/types'
22+
import {Sort} from '@influxdata/clockface'
23+
import {SortTypes, getSortedResources} from 'src/shared/utils/sort'
24+
import {DashboardSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
25+
26+
// Utils
27+
import {PaginationNav} from '@influxdata/clockface'
28+
29+
interface OwnProps {
30+
onFilterChange: (searchTerm: string) => void
31+
searchTerm: string
32+
filterComponent?: JSX.Element
33+
sortDirection: Sort
34+
sortType: SortTypes
35+
sortKey: DashboardSortKey
36+
pageHeight: number
37+
pageWidth: number
38+
dashboards: Dashboard[]
39+
totalDashboards: number
40+
}
41+
42+
type ReduxProps = ConnectedProps<typeof connector>
43+
type Props = ReduxProps & OwnProps & RouteComponentProps<{orgID: string}>
44+
45+
const DEFAULT_PAGINATION_CONTROL_HEIGHT = 62
46+
47+
@ErrorHandling
48+
class DashboardsIndexContents extends Component<Props> implements Pageable {
49+
private paginationRef: RefObject<HTMLDivElement>
50+
public currentPage: number = 1
51+
public rowsPerPage: number = 12
52+
public totalPages: number
53+
54+
private memGetSortedResources = memoizeOne<typeof getSortedResources>(
55+
getSortedResources
56+
)
57+
constructor(props) {
58+
super(props)
59+
this.paginationRef = createRef<HTMLDivElement>()
60+
}
61+
62+
public componentDidMount() {
63+
const {dashboards} = this.props
64+
65+
const dashboardIDs = dashboards.map(d => d.id)
66+
this.props.retainRangesDashTimeV1(dashboardIDs)
67+
this.props.checkDashboardLimits()
68+
69+
const params = new URLSearchParams(window.location.search)
70+
const urlPageNumber = parseInt(params.get('page'), 10)
71+
72+
const passedInPageIsValid =
73+
urlPageNumber && urlPageNumber <= this.totalPages && urlPageNumber > 0
74+
if (passedInPageIsValid) {
75+
this.currentPage = urlPageNumber
76+
}
77+
}
78+
79+
public componentDidUpdate() {
80+
// if the user filters the list while on a page that is
81+
// outside the new filtered list put them on the last page of the new list
82+
if (this.currentPage > this.totalPages) {
83+
this.paginate(this.totalPages)
84+
}
85+
}
86+
87+
public paginate = page => {
88+
this.currentPage = page
89+
const url = new URL(location.href)
90+
url.searchParams.set('page', page)
91+
history.replaceState(null, '', url.toString())
92+
this.forceUpdate()
93+
}
94+
95+
public renderDashboardCards() {
96+
const sortedDashboards = this.memGetSortedResources(
97+
this.props.dashboards,
98+
this.props.sortKey,
99+
this.props.sortDirection,
100+
this.props.sortType
101+
)
102+
103+
const startIndex = this.rowsPerPage * Math.max(this.currentPage - 1, 0)
104+
const endIndex = Math.min(
105+
startIndex + this.rowsPerPage,
106+
this.props.totalDashboards
107+
)
108+
const rows = []
109+
for (let i = startIndex; i < endIndex; i++) {
110+
const dashboard = sortedDashboards[i]
111+
if (dashboard) {
112+
rows.push(dashboard)
113+
}
114+
}
115+
return rows
116+
}
117+
118+
public render() {
119+
const {
120+
searchTerm,
121+
status,
122+
dashboards,
123+
onFilterChange,
124+
onCreateDashboard,
125+
} = this.props
126+
127+
const heightWithPagination =
128+
this.paginationRef?.current?.clientHeight ||
129+
DEFAULT_PAGINATION_CONTROL_HEIGHT
130+
const height = this.props.pageHeight - heightWithPagination
131+
132+
this.totalPages = Math.max(
133+
Math.ceil(dashboards.length / this.rowsPerPage),
134+
1
135+
)
136+
137+
if (status === RemoteDataState.Done && !dashboards.length) {
138+
return (
139+
<DashboardsTableEmpty
140+
searchTerm={searchTerm}
141+
onCreateDashboard={onCreateDashboard}
142+
summonImportFromTemplateOverlay={this.summonImportFromTemplateOverlay}
143+
summonImportOverlay={this.summonImportOverlay}
144+
/>
145+
)
146+
}
147+
148+
return (
149+
<>
150+
<ResourceList style={{width: this.props.pageWidth}}>
151+
<ResourceList.Body
152+
style={{maxHeight: height, minHeight: height, overflow: 'auto'}}
153+
emptyState={null}
154+
>
155+
<DashboardCardsPaginated
156+
dashboards={this.renderDashboardCards()}
157+
onFilterChange={onFilterChange}
158+
/>
159+
</ResourceList.Body>
160+
</ResourceList>
161+
<PaginationNav.PaginationNav
162+
ref={this.paginationRef}
163+
style={{width: this.props.pageWidth}}
164+
totalPages={this.totalPages}
165+
currentPage={this.currentPage}
166+
pageRangeOffset={1}
167+
onChange={this.paginate}
168+
/>
169+
</>
170+
)
171+
}
172+
173+
private summonImportOverlay = (): void => {
174+
const {
175+
history,
176+
match: {
177+
params: {orgID},
178+
},
179+
} = this.props
180+
history.push(`/orgs/${orgID}/dashboards-list/import`)
181+
}
182+
183+
private summonImportFromTemplateOverlay = (): void => {
184+
const {
185+
history,
186+
match: {
187+
params: {orgID},
188+
},
189+
} = this.props
190+
history.push(`/orgs/${orgID}/dashboards-list/import/template`)
191+
}
192+
}
193+
194+
const mstp = (state: AppState) => {
195+
return {
196+
status: state.resources.dashboards.status,
197+
}
198+
}
199+
200+
const mdtp = {
201+
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
202+
checkDashboardLimits: checkDashboardLimitsAction,
203+
onCreateDashboard: createDashboard as any,
204+
}
205+
206+
const connector = connect(mstp, mdtp)
207+
208+
export default connector(withRouter(DashboardsIndexContents))

0 commit comments

Comments
 (0)