import { UIRouterContext } from '@uirouter/react-hybrid'; import React from 'react'; import type { ListRowProps } from 'react-virtualized'; import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'; import type { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { ClusterPod } from './ClusterPod'; import type { Application } from '../application'; import type { IClusterGroup, IClusterSubgroup } from './filter/ClusterFilterService'; import type { ISortFilter } from '../filterModel'; import type { IStateChange } from '../reactShims'; import { ReactInjector } from '../reactShims'; import { ClusterState } from '../state'; export interface IAllClustersGroupingsProps { app: Application; initialized: boolean; } export interface IAllClustersGroupingsState { groups: IClusterSubgroup[]; sortFilter: ISortFilter; } @UIRouterContext export class AllClustersGroupings extends React.Component { private clusterFilterService = ClusterState.filterService; private clusterFilterModel = ClusterState.filterModel; private groupsSubscription: Subscription; private routeChangedSubscription: Subscription; private unwatchSortFilter: Function; private cellCache: CellMeasurerCache; private listRef: List; constructor(props: IAllClustersGroupingsProps) { super(props); this.cellCache = new CellMeasurerCache({ fixedWidth: true, minHeight: 100, defaultHeight: 177, keyMapper: (rowIndex) => { // instances have a fixed width (unless the details are shown), so use that to optimize row height measurement const group = this.state.groups[rowIndex]; const instanceCountKeys: string[] = []; const countInstances = this.clusterFilterModel.asFilterModel.sortFilter.showAllInstances; group.subgroups.forEach((subGroup) => { const subKeys: number[] = []; subGroup.serverGroups.forEach((serverGroup) => { subKeys.push(countInstances ? serverGroup.instances.length : 0); }); instanceCountKeys.push(subKeys.sort().join(',')); }); const instanceCountKey = instanceCountKeys.sort().join('; '); if (this.clusterFilterModel.asFilterModel.sortFilter.listInstances) { // NOTE: this is not perfect! If the same cluster might have different row heights for its instances and a // users gets the filters set so those different instances show up (but in the exact same quantity), this // could return an incorrect calculation. Seems unlikely, however, if you're reading this because someone is // complaining about row heights getting weird, sorry. return `${group.key}: ${instanceCountKey}`; } return instanceCountKey; }, }); this.state = { groups: this.clusterFilterModel.asFilterModel.groups.reduce((a, b) => a.concat(b.subgroups), []), sortFilter: this.clusterFilterModel.asFilterModel.sortFilter, }; } private handleWindowResize = () => { this.cellCache.clearAll(); }; private handleRouteChange = (stateChange: IStateChange) => { const { to } = stateChange; if ( to.name === 'home.applications.application.insight.clusters.instanceDetails' || to.name === 'home.applications.application.insight.clusters' ) { this.cellCache.clearAll(); } }; public componentDidMount() { window.addEventListener('resize', this.handleWindowResize); const onGroupsChanged = (groups: IClusterGroup[]) => { this.setState( { groups: groups.reduce((a, b) => a.concat(b.subgroups), []) }, () => this.listRef && this.listRef.recomputeRowHeights(0), ); }; this.groupsSubscription = this.clusterFilterService.groupsUpdatedStream.subscribe(onGroupsChanged); this.routeChangedSubscription = ReactInjector.stateEvents.stateChangeSuccess.subscribe(this.handleRouteChange); const getSortFilter = () => this.clusterFilterModel.asFilterModel.sortFilter; const onFilterChanged = ({ ...sortFilter }: any) => { const shouldResetCache = sortFilter.listInstances !== this.state.sortFilter.listInstances; this.setState({ sortFilter }, () => shouldResetCache && this.cellCache.clearAll()); }; // TODO: Remove $rootScope. Keeping it here so we can use $watch for now. // Eventually, there should be events fired when filters change. this.unwatchSortFilter = ReactInjector.$rootScope.$watch(getSortFilter, onFilterChanged, true); this.scrollToRow(); } public componentWillUnmount() { window.removeEventListener('resize', this.handleWindowResize); this.groupsSubscription.unsubscribe(); this.routeChangedSubscription.unsubscribe(); this.unwatchSortFilter(); } private scrollToRow = () => { const { $stateParams } = ReactInjector; // Automatically scroll server group into view if deep linkedif ($stateParams.serverGroup) { this.clusterFilterService.groupsUpdatedStream.pipe(take(1)).subscribe(() => { const scrollToRow = this.state.groups.findIndex((group) => group.subgroups.some((subgroup) => subgroup.serverGroups.some( (sg) => sg.account === $stateParams.accountId && sg.name === $stateParams.serverGroup && sg.region === $stateParams.region, ), ), ); if (scrollToRow >= 0) { this.listRef.scrollToRow(scrollToRow); } }); }; private renderRow = (props: ListRowProps): JSX.Element => { const { app } = this.props; const { groups = [], sortFilter } = this.state; const group = groups[props.index]; const parent: React.ReactType = props.parent as any; // bad cast, sad @types return (
); }; private noRowsRender = (): JSX.Element => { const dataSource = this.props.app.getDataSource('serverGroups'); if (!this.props.initialized || dataSource.loadFailure) { return null; } if (!dataSource.data.length && !dataSource.fetchOnDemand) { return

No server groups found in this application

; } if (this.props.app.getDataSource('serverGroups').fetchOnDemand) { const filtered = Object.keys(this.state.sortFilter.clusters).length; if (!filtered) { return null; } } return

No server groups match the filters you've selected

; }; private setListRef = (listRef: List) => { this.listRef = listRef; }; public render() { const { groups = [] } = this.state; return ( {({ width, height }) => ( )} ); } }