import classNames from 'classnames'; import { isEqual } from 'lodash'; import React from 'react'; import { Subject } from 'rxjs'; import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import type { IInstance, ILoadBalancerHealth, IServerGroup } from '../domain'; import { Tooltip } from '../presentation'; import { ReactInjector } from '../reactShims'; import { ClusterState } from '../state'; import { timestamp } from '../utils/timeFormatters'; export interface IInstanceListBodyProps { serverGroup: IServerGroup; instances: IInstance[]; hasDiscovery: boolean; hasLoadBalancers: boolean; } export interface IInstanceListBodyState { selectedInstanceIds: string[]; activeInstanceId: string; multiselect: boolean; instanceSort?: string; } export class InstanceListBody extends React.Component { private $uiRouter = ReactInjector.$uiRouter; private $state = ReactInjector.$state; private destroy$ = new Subject(); constructor(props: IInstanceListBodyProps) { super(props); this.state = { selectedInstanceIds: this.getSelectedInstanceIds(), activeInstanceId: this.$state.params.instanceId, multiselect: this.$state.params.multiselect, instanceSort: this.$state.params.instanceSort, }; } public componentDidMount() { ClusterState.multiselectModel.instancesStream.pipe(takeUntil(this.destroy$)).subscribe(() => { this.setState({ selectedInstanceIds: this.getSelectedInstanceIds() }); }); this.$uiRouter.globals.params$ .pipe( map((params) => [params.instanceId, params.multiselect, params.instanceSort]), distinctUntilChanged(isEqual), takeUntil(this.destroy$), ) .subscribe(() => { const { params } = this.$state; this.setState({ activeInstanceId: params.instanceId, multiselect: params.multiselect, instanceSort: params.instanceSort, }); }); } private getSelectedInstanceIds(): string[] { const { instances, serverGroup } = this.props; if (this.$state.params.multiselect) { return instances .filter((i) => ClusterState.multiselectModel.instanceIsSelected(serverGroup, i.id)) .map((i) => i.id); } return []; } public componentWillUnmount() { this.destroy$.next(); } public shouldComponentUpdate(nextProps: IInstanceListBodyProps, nextState: IInstanceListBodyState): boolean { if (this.props.serverGroup.stringVal !== nextProps.serverGroup.stringVal) { return true; } if ( this.props.instances .map((i) => i.id) .sort() .join(',') !== nextProps.instances .map((i) => i.id) .sort() .join(',') ) { return true; } const { state } = this; return ( state.activeInstanceId !== nextState.activeInstanceId || state.selectedInstanceIds.sort().join(',') !== nextState.selectedInstanceIds.sort().join(',') || state.multiselect !== nextState.multiselect || state.instanceSort !== nextState.instanceSort ); } private instanceSorter = (a1: IInstance, b1: IInstance): number => { const { instanceSort = 'launchTime' } = this.state; const filterSplit = instanceSort.split('-'); const filterType = filterSplit.length === 1 ? filterSplit[0] : filterSplit[1]; const reverse = filterSplit.length === 2; const a = reverse ? b1 : a1; const b = reverse ? a1 : b1; switch (filterType) { case 'id': return a.id.localeCompare(b.id); case 'launchTime': return a.launchTime === b.launchTime ? a.id.localeCompare(b.id) : a.launchTime - b.launchTime; case 'availabilityZone': return a.availabilityZone === b.availabilityZone ? a.launchTime === b.launchTime ? a.id.localeCompare(b.id) : a.launchTime - b.launchTime : a.availabilityZone.localeCompare(b.availabilityZone); case 'discoveryState': { const aHealth = (a.health || []).filter((health) => health.type === 'Discovery'); const bHealth = (b.health || []).filter((health) => health.type === 'Discovery'); if (aHealth.length && !bHealth.length) { return -1; } if (!aHealth.length && bHealth.length) { return 1; } return (!aHealth.length && !bHealth.length) || aHealth[0].state === bHealth[0].state ? a.launchTime === b.launchTime ? a.id.localeCompare(b.id) : a.launchTime - b.launchTime : aHealth[0].state.localeCompare(bHealth[0].state); } case 'loadBalancerSort': { const aHealth2 = (a.health || []).filter((health) => health.type === 'LoadBalancer'); const bHealth2 = (b.health || []).filter((health) => health.type === 'LoadBalancer'); if (aHealth2.length && !bHealth2.length) { return -1; } if (!aHealth2.length && bHealth2.length) { return 1; } const aHealthStr = aHealth2.map((h) => h.loadBalancers.map((l) => l.name + ':' + l.state)).join(','); const bHealthStr = bHealth2.map((h) => h.loadBalancers.map((l) => l.name + ':' + l.state)).join(','); return aHealthStr === bHealthStr ? a.launchTime === b.launchTime ? a.id.localeCompare(b.id) : a.launchTime - b.launchTime : aHealthStr.localeCompare(bHealthStr); } default: return -1; } }; private renderRow(instance: IInstance): JSX.Element { const { hasLoadBalancers, hasDiscovery } = this.props; const showProviderHealth = !hasLoadBalancers && !hasDiscovery; const healthMetrics = instance.health || []; let discoveryStatus = '-'; let providerStatus = ''; let loadBalancers: ILoadBalancerHealth[] = []; healthMetrics.forEach((health) => { if (hasLoadBalancers && health.type === 'LoadBalancer') { loadBalancers = health.loadBalancers; } if (hasDiscovery && health.type === 'Discovery') { discoveryStatus = health.state; } if (showProviderHealth) { providerStatus = health.state; } }); const isActive = this.state.activeInstanceId === instance.id; const rowClass = classNames({ clickable: true, active: isActive, }); return ( {this.$state.params.multiselect && ( )} {instance.name || instance.id} {timestamp(instance.launchTime)} {instance.availabilityZone} {hasDiscovery && {discoveryStatus}} {hasLoadBalancers && this.renderLoadBalancerCell(loadBalancers)} {showProviderHealth && {providerStatus}} ); } private renderLoadBalancerCell(loadBalancerHealths: ILoadBalancerHealth[]): JSX.Element { return ( {loadBalancerHealths.length === 0 && -} {loadBalancerHealths.map((h) => { const tooltip = h.state === 'OutOfService' ? h.description.replace(/"/g, '"') : null; const icon = h.healthState === 'Up' || h.state === 'InService' ? 'Up' : 'Down'; return (
{tooltip && (
{h.name}
)} {!tooltip && {h.name}}
); })} ); } private instanceBodyClicked = (event: React.MouseEvent): void => { const target = event.target as HTMLElement; const targetRow = target.closest('tr'); if (!targetRow) { return; } ClusterState.multiselectModel.toggleInstance(this.props.serverGroup, targetRow.getAttribute('data-instance-id')); event.stopPropagation(); }; public render() { return ( {this.props.instances.sort(this.instanceSorter).map((i) => this.renderRow(i))} ); } }