diff --git a/src/dashboards/components/dashboard_index/DashboardCardsPaginated.tsx b/src/dashboards/components/dashboard_index/DashboardCardsPaginated.tsx new file mode 100644 index 0000000000..fe0e86e60f --- /dev/null +++ b/src/dashboards/components/dashboard_index/DashboardCardsPaginated.tsx @@ -0,0 +1,96 @@ +// Libraries +import React, {PureComponent} from 'react' +import {connect} from 'react-redux' + +// Components +import DashboardCard from 'src/dashboards/components/dashboard_index/DashboardCard' +import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert' + +// Types +import {AppState, Dashboard} from 'src/types' +import {LimitStatus} from 'src/cloud/actions/limits' + +// Utils +import {extractDashboardLimits} from 'src/cloud/utils/limits' +import {isFlagEnabled} from 'src/shared/utils/featureFlag' +import {CLOUD} from 'src/shared/constants' + +let getPinnedItems +if (CLOUD) { + getPinnedItems = require('src/shared/contexts/pinneditems').getPinnedItems +} + +interface StateProps { + limitStatus: LimitStatus['status'] +} + +interface OwnProps { + dashboards: Dashboard[] + onFilterChange: (searchTerm: string) => void +} + +class DashboardCards extends PureComponent { + private _isMounted = true + + state = { + pinnedItems: [], + } + + public componentDidMount() { + if (isFlagEnabled('pinnedItems') && CLOUD) { + getPinnedItems() + .then(res => { + if (this._isMounted) { + this.setState(prev => ({...prev, pinnedItems: res})) + } + }) + .catch(err => { + console.error(err) + }) + } + } + + public componentWillUnmount() { + this._isMounted = false + } + + public render() { + const {dashboards, onFilterChange} = this.props + + const {pinnedItems} = this.state + + return ( +
+
+ {dashboards.map(({id, name, description, labels, meta}) => ( + item?.metadata.dashboardID === id) + } + /> + ))} + +
+
+ ) + } +} + +const mstp = (state: AppState) => { + return { + limitStatus: extractDashboardLimits(state), + } +} + +export default connect(mstp)(DashboardCards) diff --git a/src/dashboards/components/dashboard_index/DashboardsIndexContentsPaginated.tsx b/src/dashboards/components/dashboard_index/DashboardsIndexContentsPaginated.tsx new file mode 100644 index 0000000000..fca237bb53 --- /dev/null +++ b/src/dashboards/components/dashboard_index/DashboardsIndexContentsPaginated.tsx @@ -0,0 +1,208 @@ +// Libraries +import React, {Component, createRef, RefObject} from 'react' +import {connect, ConnectedProps} from 'react-redux' +import {withRouter, RouteComponentProps} from 'react-router-dom' +import memoizeOne from 'memoize-one' + +// Components +import DashboardsTableEmpty from 'src/dashboards/components/dashboard_index/DashboardsTableEmpty' +import DashboardCardsPaginated from 'src/dashboards/components/dashboard_index/DashboardCardsPaginated' +import {ResourceList} from '@influxdata/clockface' + +// Actions +import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/ranges' +import {checkDashboardLimits as checkDashboardLimitsAction} from 'src/cloud/actions/limits' +import {createDashboard} from 'src/dashboards/actions/thunks' + +// Decorators +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Types +import {Dashboard, AppState, Pageable, RemoteDataState} from 'src/types' +import {Sort} from '@influxdata/clockface' +import {SortTypes, getSortedResources} from 'src/shared/utils/sort' +import {DashboardSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems' + +// Utils +import {PaginationNav} from '@influxdata/clockface' + +interface OwnProps { + onFilterChange: (searchTerm: string) => void + searchTerm: string + filterComponent?: JSX.Element + sortDirection: Sort + sortType: SortTypes + sortKey: DashboardSortKey + pageHeight: number + pageWidth: number + dashboards: Dashboard[] + totalDashboards: number +} + +type ReduxProps = ConnectedProps +type Props = ReduxProps & OwnProps & RouteComponentProps<{orgID: string}> + +const DEFAULT_PAGINATION_CONTROL_HEIGHT = 62 + +@ErrorHandling +class DashboardsIndexContents extends Component implements Pageable { + private paginationRef: RefObject + public currentPage: number = 1 + public rowsPerPage: number = 12 + public totalPages: number + + private memGetSortedResources = memoizeOne( + getSortedResources + ) + constructor(props) { + super(props) + this.paginationRef = createRef() + } + + public componentDidMount() { + const {dashboards} = this.props + + const dashboardIDs = dashboards.map(d => d.id) + this.props.retainRangesDashTimeV1(dashboardIDs) + this.props.checkDashboardLimits() + + const params = new URLSearchParams(window.location.search) + const urlPageNumber = parseInt(params.get('page'), 10) + + const passedInPageIsValid = + urlPageNumber && urlPageNumber <= this.totalPages && urlPageNumber > 0 + if (passedInPageIsValid) { + this.currentPage = urlPageNumber + } + } + + public componentDidUpdate() { + // if the user filters the list while on a page that is + // outside the new filtered list put them on the last page of the new list + if (this.currentPage > this.totalPages) { + this.paginate(this.totalPages) + } + } + + public paginate = page => { + this.currentPage = page + const url = new URL(location.href) + url.searchParams.set('page', page) + history.replaceState(null, '', url.toString()) + this.forceUpdate() + } + + public renderDashboardCards() { + const sortedDashboards = this.memGetSortedResources( + this.props.dashboards, + this.props.sortKey, + this.props.sortDirection, + this.props.sortType + ) + + const startIndex = this.rowsPerPage * Math.max(this.currentPage - 1, 0) + const endIndex = Math.min( + startIndex + this.rowsPerPage, + this.props.totalDashboards + ) + const rows = [] + for (let i = startIndex; i < endIndex; i++) { + const dashboard = sortedDashboards[i] + if (dashboard) { + rows.push(dashboard) + } + } + return rows + } + + public render() { + const { + searchTerm, + status, + dashboards, + onFilterChange, + onCreateDashboard, + } = this.props + + const heightWithPagination = + this.paginationRef?.current?.clientHeight || + DEFAULT_PAGINATION_CONTROL_HEIGHT + const height = this.props.pageHeight - heightWithPagination + + this.totalPages = Math.max( + Math.ceil(dashboards.length / this.rowsPerPage), + 1 + ) + + if (status === RemoteDataState.Done && !dashboards.length) { + return ( + + ) + } + + return ( + <> + + + + + + + + ) + } + + private summonImportOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import`) + } + + private summonImportFromTemplateOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import/template`) + } +} + +const mstp = (state: AppState) => { + return { + status: state.resources.dashboards.status, + } +} + +const mdtp = { + retainRangesDashTimeV1: retainRangesDashTimeV1Action, + checkDashboardLimits: checkDashboardLimitsAction, + onCreateDashboard: createDashboard as any, +} + +const connector = connect(mstp, mdtp) + +export default connector(withRouter(DashboardsIndexContents)) diff --git a/src/dashboards/components/dashboard_index/DashboardsIndexPaginated.tsx b/src/dashboards/components/dashboard_index/DashboardsIndexPaginated.tsx new file mode 100644 index 0000000000..8e7108b304 --- /dev/null +++ b/src/dashboards/components/dashboard_index/DashboardsIndexPaginated.tsx @@ -0,0 +1,278 @@ +// Libraries +import React, {PureComponent} from 'react' +import {Route, RouteComponentProps, Switch} from 'react-router-dom' +import {connect, ConnectedProps} from 'react-redux' +import {AutoSizer} from 'react-virtualized' + +// Decorators +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Components +import DashboardsIndexContents from 'src/dashboards/components/dashboard_index/DashboardsIndexContentsPaginated' +import { + ComponentSize, + Page, + Sort, + SpinnerContainer, + TechnoSpinner, +} from '@influxdata/clockface' +import FilterList from 'src/shared/components/FilterList' +import SearchWidget from 'src/shared/components/search_widget/SearchWidget' +import AddResourceDropdown from 'src/shared/components/AddResourceDropdown' +import GetAssetLimits from 'src/cloud/components/GetAssetLimits' +import RateLimitAlert from 'src/cloud/components/RateLimitAlert' +import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown' +import DashboardImportOverlay from 'src/dashboards/components/DashboardImportOverlay' +import CreateFromTemplateOverlay from 'src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay' +import DashboardExportOverlay from 'src/dashboards/components/DashboardExportOverlay' + +// Utils +import {pageTitleSuffixer} from 'src/shared/utils/pageTitles' +import {extractDashboardLimits} from 'src/cloud/utils/limits' + +// Actions +import { + createDashboard as createDashboardAction, + getDashboards, +} from 'src/dashboards/actions/thunks' +import {setDashboardSort, setSearchTerm} from 'src/dashboards/actions/creators' +import {getLabels} from 'src/labels/actions/thunks' +import {getAll} from 'src/resources/selectors' +import {getResourcesStatus} from 'src/resources/selectors/getResourcesStatus' + +// Types +import {AppState, ResourceType, Dashboard} from 'src/types' +import {SortTypes} from 'src/shared/utils/sort' +import {DashboardSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems' + +import ErrorBoundary from 'src/shared/components/ErrorBoundary' + +type ReduxProps = ConnectedProps +type Props = ReduxProps & RouteComponentProps<{orgID: string}> + +interface State { + searchTerm: string +} + +const FilterDashboards = FilterList() + +@ErrorHandling +class DashboardIndex extends PureComponent { + constructor(props: Props) { + super(props) + + this.state = { + searchTerm: props.searchTerm, + } + } + + componentDidMount() { + this.props.getDashboards() + this.props.getLabels() + + let sortType: SortTypes = this.props.sortOptions.sortType + const params = new URLSearchParams(window.location.search) + let sortKey: DashboardSortKey = 'name' + if (params.get('sortKey') === 'name') { + sortKey = 'name' + } else if (params.get('sortKey') === 'meta.updatedAt') { + sortKey = 'meta.updatedAt' + sortType = SortTypes.Date + } + + let sortDirection: Sort = this.props.sortOptions.sortDirection + if (params.get('sortDirection') === Sort.Ascending) { + sortDirection = Sort.Ascending + } else if (params.get('sortDirection') === Sort.Descending) { + sortDirection = Sort.Descending + } + + let searchTerm: string = '' + if (params.get('searchTerm') !== null) { + searchTerm = params.get('searchTerm') + this.props.setSearchTerm(searchTerm) + this.setState({searchTerm}) + } + + this.props.setDashboardSort({sortKey, sortDirection, sortType}) + } + + componentWillUnmount() { + this.props.setSearchTerm(this.state.searchTerm) + } + + public render() { + const { + createDashboard, + sortOptions, + limitStatus, + dashboards, + remoteDataState, + } = this.props + const {searchTerm} = this.state + + return ( + } + > + + + + + + + + + + + + + + + + + + + + {({width, height}) => { + return ( + + + {filteredDashboards => ( + + )} + + + ) + }} + + + + + + + + + + + ) + } + + private handleFilterDashboards = (searchTerm: string): void => { + const url = new URL(location.href) + url.searchParams.set('searchTerm', searchTerm) + history.replaceState(null, '', url.toString()) + + this.setState({searchTerm}) + } + + private handleSort = ( + sortKey: DashboardSortKey, + sortDirection: Sort, + sortType: SortTypes + ): void => { + const url = new URL(location.href) + url.searchParams.set('sortKey', sortKey) + url.searchParams.set('sortDirection', sortDirection) + history.replaceState(null, '', url.toString()) + + this.props.setDashboardSort({sortKey, sortDirection, sortType}) + } + + private summonImportOverlay = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/dashboards-list/import`) + } + + private summonTemplatePage = (): void => { + const { + history, + match: { + params: {orgID}, + }, + } = this.props + history.push(`/orgs/${orgID}/settings/templates`) + } +} + +const mstp = (state: AppState) => { + const {sortOptions, searchTerm} = state.resources.dashboards + const remoteDataState = getResourcesStatus(state, [ + ResourceType.Dashboards, + ResourceType.Labels, + ]) + return { + dashboards: getAll(state, ResourceType.Dashboards), + limitStatus: extractDashboardLimits(state), + sortOptions, + searchTerm, + remoteDataState, + } +} + +const mdtp = { + createDashboard: createDashboardAction, + setDashboardSort, + setSearchTerm, + getDashboards, + getLabels, +} + +const connector = connect(mstp, mdtp) + +export default connector(DashboardIndex) diff --git a/src/shared/containers/SetOrg.tsx b/src/shared/containers/SetOrg.tsx index aa1c9f4613..18cb02a3c0 100644 --- a/src/shared/containers/SetOrg.tsx +++ b/src/shared/containers/SetOrg.tsx @@ -15,6 +15,7 @@ import { TaskRunsPagePaginated, TaskEditPage, DashboardsIndex, + DashboardsIndexPaginated, DataExplorerPage, DashboardContainer, FlowPage, @@ -180,12 +181,18 @@ const SetOrg: FC = () => { path={`${orgPath}/data-explorer`} component={DataExplorerPage} /> - {/* Dashboards */} - + {isFlagEnabled('paginatedDashboards') ? ( + + ) : ( + + )} export const DashboardsIndex = lazy(() => import('src/dashboards/components/dashboard_index/DashboardsIndex') ) +export const DashboardsIndexPaginated = lazy(() => + import('src/dashboards/components/dashboard_index/DashboardsIndexPaginated') +) export const DashboardContainer = lazy(() => import('src/dashboards/components/DashboardContainer') )