diff --git a/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx b/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx index 8c077c6e34..72a8c34362 100644 --- a/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx +++ b/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx @@ -11,50 +11,103 @@ import Service from "../structs/Service"; import ServiceTree from "../structs/ServiceTree"; import { SortDirection } from "plugins/services/src/js/types/SortDirection"; import ServiceTableUtil from "../utils/ServiceTableUtil"; +import { getStatusIconProps } from "../utils/ServiceStatusIconUtil"; const StatusMapping: any = { Running: "running-state" }; +interface StatusColumnProps { + statusText: string; + instancesCount: number; + runningInstances: number; + timeWaiting: string | null; + timeQueued: number | null; + serviceStatus: object; + isServiceTree: boolean; + isService: boolean; + id: string | number; + displayDeclinedOffers: boolean; + appsWithWarnings: number | null; + unableToLaunch: boolean; +} + +const StatusColumn = React.memo( + ({ + statusText, + instancesCount, + runningInstances, + timeWaiting, + timeQueued, + serviceStatus, + isService, + isServiceTree, + id, + displayDeclinedOffers, + appsWithWarnings, + unableToLaunch + }: StatusColumnProps) => { + const serviceStatusClassSet: string = StatusMapping[statusText] || ""; + // L10NTODO: Pluralize + const tooltipContent = ( + + {runningInstances} {StringUtil.pluralize("instance", runningInstances)}{" "} + running out of {instancesCount} + + ); + const hasStatusText = statusText !== (ServiceStatus as any).NA.displayName; + + return ( + + + + + {hasStatusText && ( + + )} + + + + + + + ); + } +); + export function statusRenderer( service: Service | ServiceTree ): React.ReactNode { - const serviceStatusText: string = service.getStatus(); - const serviceStatusClassSet: string = StatusMapping[serviceStatusText] || ""; - const instancesCount = service.getInstancesCount() as number; - const runningInstances = service.getRunningInstancesCount() as number; - // L10NTODO: Pluralize - const tooltipContent = ( - - {runningInstances} {StringUtil.pluralize("instance", runningInstances)}{" "} - running out of {instancesCount} - - ); - const hasStatusText = serviceStatusText !== ServiceStatus.NA.displayName; + const iconProps = getStatusIconProps(service); - return ( - - - - - {hasStatusText && ( - - )} - - - - - - - ); + const props = { + statusText: service.getStatus(), + instancesCount: service.getInstancesCount(), + runningInstances: service.getRunningInstancesCount() + }; + return ; } export function statusSorter( diff --git a/plugins/services/src/js/components/ServiceBreadcrumbs.js b/plugins/services/src/js/components/ServiceBreadcrumbs.js index 1b42314386..d8207aaced 100644 --- a/plugins/services/src/js/components/ServiceBreadcrumbs.js +++ b/plugins/services/src/js/components/ServiceBreadcrumbs.js @@ -142,6 +142,7 @@ class ServiceBreadcrumbs extends React.Component { let iconDisplay = null; const instancesCount = service.getInstancesCount(); const runningInstances = service.getRunningInstancesCount(); + const serviceStatus = service.getServiceStatus(); const tooltipContent = ( - + ); } diff --git a/plugins/services/src/js/components/ServiceList.js b/plugins/services/src/js/components/ServiceList.js index 0ad7114366..01d03e64ab 100644 --- a/plugins/services/src/js/components/ServiceList.js +++ b/plugins/services/src/js/components/ServiceList.js @@ -7,6 +7,7 @@ import createReactClass from "create-react-class"; import { Link, routerShape } from "react-router"; import ServiceStatusIcon from "./ServiceStatusIcon"; +import { getStatusIconProps } from "../utils/ServiceStatusIconUtil"; const ServiceList = createReactClass({ displayName: "ServiceList", @@ -50,6 +51,7 @@ const ServiceList = createReactClass({ return services.map(service => { const instancesCount = service.getInstancesCount(); const runningInstances = service.getRunningInstancesCount(); + const iconProps = getStatusIconProps(service); const tooltipContent = ( ), tag: "div" diff --git a/plugins/services/src/js/components/ServiceStatusIcon.tsx b/plugins/services/src/js/components/ServiceStatusIcon.tsx index 91fb6e26b2..0017661d75 100644 --- a/plugins/services/src/js/components/ServiceStatusIcon.tsx +++ b/plugins/services/src/js/components/ServiceStatusIcon.tsx @@ -12,15 +12,9 @@ import StringUtil from "#SRC/js/utils/StringUtil"; // @ts-ignore import DeclinedOffersUtil from "../utils/DeclinedOffersUtil"; -import { Status } from "../constants/ServiceStatus"; -import Pod from "../structs/Pod"; -import Service from "../structs/Service"; -import ServiceTree from "../structs/ServiceTree"; -const UNABLE_TO_LAUNCH_TIMEOUT = 1000 * 60 * 30; // 30 minutes - -const getTooltipContent = (service: Service, content: JSX.Element) => { - const servicePath = encodeURIComponent(service.getId()); +const getTooltipContent = (id: string, content: JSX.Element) => { + const servicePath = encodeURIComponent(id); return ( @@ -35,35 +29,44 @@ const getTooltipContent = (service: Service, content: JSX.Element) => { ); }; -type TreeNode = Service | ServiceTree; - -class ServiceStatusIcon extends React.Component<{ - showTooltip?: boolean; +interface ServiceStatusIconProps { + showTooltip: boolean; tooltipContent: JSX.Element; - service: TreeNode; -}> { + id: string | number; + isService: boolean; + isServiceTree: boolean; + serviceStatus: any; + timeWaiting: string | null; + timeQueued: number | null; + appsWithWarnings: number | null; + displayDeclinedOffers: boolean; + unableToLaunch: boolean; +} + +class ServiceStatusIcon extends React.Component { static propTypes = { showTooltip: PropTypes.bool, tooltipContent: PropTypes.node, - service: PropTypes.oneOfType([ - PropTypes.instanceOf(Service), - PropTypes.instanceOf(ServiceTree), - PropTypes.instanceOf(Pod) - ]) + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + isService: PropTypes.bool, + isServiceTree: PropTypes.bool, + serviceStatus: PropTypes.object, + timeWaiting: PropTypes.string, + timeQueued: PropTypes.number, + appsWithWarnings: PropTypes.number, + displayDeclinedOffers: PropTypes.bool, + unableToLaunch: PropTypes.bool }; - getDeclinedOffersWarning(service: TreeNode) { - if (DeclinedOffersUtil.displayDeclinedOffersWarning(service)) { - const timeWaiting = - Date.now() - - DateUtil.strToMs(DeclinedOffersUtil.getTimeWaiting( - service.getQueue() - ) as string); + getDeclinedOffersWarning() { + const { displayDeclinedOffers, timeWaiting } = this.props; + if (displayDeclinedOffers) { + const waitDuration = Date.now() - (DateUtil.strToMs(timeWaiting) || 0); return this.getTooltip( DC/OS has been waiting for resources and is unable to complete this - deployment for {DateUtil.getDuration(timeWaiting)}. + deployment for {DateUtil.getDuration(waitDuration)}. ); } @@ -71,29 +74,16 @@ class ServiceStatusIcon extends React.Component<{ return null; } - getServiceTreeWarning(serviceTree: ServiceTree) { - const appsWithWarningsCount = serviceTree - .filterItems(item => { - if (!(item instanceof ServiceTree)) { - return ( - DeclinedOffersUtil.displayDeclinedOffersWarning(item) || - this.isUnableToLaunch(item) - ); - } - - return false; - }) - .flattenItems() - .getItems().length; + getServiceTreeWarning() { + const { appsWithWarnings } = this.props; // L10NTODO: Pluralize - if (appsWithWarningsCount > 0) { + if (appsWithWarnings != null && appsWithWarnings > 0) { return this.getTooltip( DC/OS is waiting for resources and is unable to complete the - deployment of {appsWithWarningsCount}{" "} - {StringUtil.pluralize("service", appsWithWarningsCount)} in this - group. + deployment of {appsWithWarnings}{" "} + {StringUtil.pluralize("service", appsWithWarnings)} in this group. ); } @@ -102,12 +92,12 @@ class ServiceStatusIcon extends React.Component<{ } getTooltip(content: JSX.Element) { - const { service } = this.props; + const { id, isService } = this.props; let icon = ; - if (service instanceof Service) { - const servicePath = encodeURIComponent(service.getId()); - content = getTooltipContent(service, content); + if (isService) { + const servicePath = encodeURIComponent(`${id}`); + content = getTooltipContent(`${id}`, content); icon = {icon}; } @@ -123,30 +113,19 @@ class ServiceStatusIcon extends React.Component<{ ); } - getUnableToLaunchWarning(service: Service) { - const duration = DateUtil.getDuration( - Date.now() - DateUtil.strToMs((service.getQueue() as any).since as string) - ); - - return this.getTooltip( - - There are tasks in this queue that DC/OS has failed to deploy for - {duration}. - - ); - } - - isUnableToLaunch(service: TreeNode) { - const queue: any = service.getQueue(); - - if (queue == null) { - return false; + getUnableToLaunchWarning() { + const { unableToLaunch, timeQueued } = this.props; + const duration = DateUtil.getDuration(Date.now() - (timeQueued || 0)); + if (unableToLaunch) { + return this.getTooltip( + + There are tasks in this queue that DC/OS has failed to deploy for{" "} + {duration}. + + ); } - return ( - Date.now() - (DateUtil.strToMs(queue.since) as number) >= - UNABLE_TO_LAUNCH_TIMEOUT - ); + return null; } renderIcon(iconState: any) { @@ -169,8 +148,12 @@ class ServiceStatusIcon extends React.Component<{ } render() { - const { service } = this.props; - const serviceStatus: Status | null = service.getServiceStatus(); + const { + serviceStatus, + isServiceTree, + displayDeclinedOffers, + unableToLaunch + } = this.props; const iconState = serviceStatus && serviceStatus.icon; // Catch other cases instead throwing a warning/error @@ -178,18 +161,18 @@ class ServiceStatusIcon extends React.Component<{ return null; } - if (service instanceof ServiceTree) { - return this.getServiceTreeWarning(service) || this.renderIcon(iconState); + if (isServiceTree) { + return this.getServiceTreeWarning() || this.renderIcon(iconState); } // Display the declined offers warning if that's causing a delay. - if (DeclinedOffersUtil.displayDeclinedOffersWarning(service)) { - return this.getDeclinedOffersWarning(service); + if (displayDeclinedOffers) { + return this.getDeclinedOffersWarning(); } // If not, we try to display an unable to launch warning, then default to nothing. - if (this.isUnableToLaunch(service)) { - return this.getUnableToLaunchWarning(service); + if (unableToLaunch) { + return this.getUnableToLaunchWarning(); } return this.renderIcon(iconState); diff --git a/plugins/services/src/js/components/ServiceStatusProgressBar.tsx b/plugins/services/src/js/components/ServiceStatusProgressBar.tsx index 911c1a940a..2bcf8aa6aa 100644 --- a/plugins/services/src/js/components/ServiceStatusProgressBar.tsx +++ b/plugins/services/src/js/components/ServiceStatusProgressBar.tsx @@ -3,51 +3,44 @@ import * as React from "react"; import { Tooltip } from "reactjs-components"; import ProgressBar from "#SRC/js/components/ProgressBar"; -import Pod from "../structs/Pod"; -import Service from "../structs/Service"; -import ServiceTree from "../structs/ServiceTree"; import * as ServiceStatus from "../constants/ServiceStatus"; const { Plural } = require("@lingui/macro"); interface ServiceStatusProgressBarProps { - service: Service | ServiceTree | Pod; + runningInstances: number; + instancesCount: number; + serviceStatus: any; } class ServiceStatusProgressBar extends React.Component< ServiceStatusProgressBarProps > { static propTypes = { - service: PropTypes.oneOfType([ - PropTypes.instanceOf(Service), - PropTypes.instanceOf(ServiceTree), - PropTypes.instanceOf(Pod) - ]).isRequired + runningInstances: PropTypes.number.isRequired, + instancesCount: PropTypes.number.isRequired, + serviceStatus: PropTypes.object.isRequired }; getTooltipContent() { - const { service } = this.props; - const runningInstances = service.getRunningInstancesCount(); - const instancesTotal = service.getInstancesCount(); + const { runningInstances, instancesCount } = this.props; return ( ); } render() { - const { service } = this.props; - const instancesCount = service.getInstancesCount(); - const runningInstances = service.getRunningInstancesCount(); + const { instancesCount, serviceStatus, runningInstances } = this.props; - if (!ServiceStatus.showProgressBar(service.getServiceStatus())) { + if (!ServiceStatus.showProgressBar(serviceStatus)) { return null; } diff --git a/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js b/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js index 1191e10969..a70e6be52f 100644 --- a/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js +++ b/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js @@ -22,40 +22,47 @@ let thisInstance; describe("ServiceStatusProgressBar", function() { beforeEach(function() { - thisInstance = shallow(); + thisInstance = shallow( + + ); }); describe("ProgressBar", function() { it("display ProgressBar", function() { + const pod = new Pod({ + env: { + SDK_UNINSTALL: true + }, + spec: { + scaling: { + instances: 10, + kind: "fixed" + } + }, + instances: [ + { + status: "stable" + }, + { + status: "stable" + }, + { + status: "stable" + }, + { + status: "stable" + } + ] + }); thisInstance = shallow( ); @@ -68,13 +75,19 @@ describe("ServiceStatusProgressBar", function() { describe("Tooltip", function() { it("display Tooltip", function() { - const app = new Application({ + const service = new Application({ queue: { overdue: true } }); - thisInstance = shallow(); + thisInstance = shallow( + + ); expect(expect(thisInstance.find(Tooltip).exists())).toBeTruthy(); }); @@ -87,7 +100,7 @@ describe("ServiceStatusProgressBar", function() { .props().values; expect(childrenContent).toMatchObject({ - instancesTotal: 1, + instancesCount: 1, runningInstances: 0 }); }); diff --git a/plugins/services/src/js/containers/pod-detail/PodHeader.js b/plugins/services/src/js/containers/pod-detail/PodHeader.js index 6c1180eb2a..cd73aa8d86 100644 --- a/plugins/services/src/js/containers/pod-detail/PodHeader.js +++ b/plugins/services/src/js/containers/pod-detail/PodHeader.js @@ -109,6 +109,8 @@ class PodHeader extends React.Component { getSubHeader(pod) { const serviceHealth = pod.getHealth(); const serviceStatus = pod.getServiceStatus(); + const runningInstances = pod.getRunningInstancesCount(); + const instancesCount = pod.getInstancesCount(); const tasksSummary = pod.getTasksSummary(); const serviceStatusClassSet = StatusMapping[serviceStatus.displayName] || ""; @@ -130,7 +132,14 @@ class PodHeader extends React.Component { shouldShow: runningTasksCount != null && runningTasksSubHeader != null }, { - label: , + label: ( + + ), shouldShow: true } ].map(function(item, index) { diff --git a/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js b/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js index d2e6fdb518..8b481ba588 100644 --- a/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js +++ b/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js @@ -1,5 +1,3 @@ -import { NumberCell } from "@dcos/ui-kit"; - import { regionRenderer } from "../../../columns/ServicesTableRegionColumn"; import { cpuRenderer } from "../../../columns/ServicesTableCPUColumn"; import { memRenderer } from "../../../columns/ServicesTableMemColumn"; @@ -15,14 +13,6 @@ const React = require("react"); const renderer = require("react-test-renderer"); const Application = require("../../../structs/Application"); -function renderNumberCell(value) { - return ( - - {value} - - ); -} - describe("ServicesTable", function() { const healthyService = new Application({ healthChecks: [{ path: "", protocol: "HTTP" }], @@ -39,27 +29,27 @@ describe("ServicesTable", function() { describe("#renderStats", function() { it("renders resources/stats cpus property", function() { - var cpusCell = cpuRenderer(healthyService); + var cpusCell = renderer.create(cpuRenderer(healthyService)).toJSON(); - expect(cpusCell).toEqual(renderNumberCell(1)); + expect(cpusCell).toMatchSnapshot(); }); it("renders resources/stats gpus property", function() { - var gpusCell = gpuRenderer(healthyService); + var gpusCell = renderer.create(gpuRenderer(healthyService)).toJSON(); - expect(gpusCell).toEqual(renderNumberCell(1)); + expect(gpusCell).toMatchSnapshot(); }); it("renders resources/stats mem property", function() { - var memCell = memRenderer(healthyService); + var memCell = renderer.create(memRenderer(healthyService)).toJSON(); - expect(memCell).toEqual(renderNumberCell("2 GiB")); + expect(memCell).toMatchSnapshot(); }); it("renders resources/stats disk property", function() { - var disksCell = diskRenderer(healthyService); + var disksCell = renderer.create(diskRenderer(healthyService)).toJSON(); - expect(disksCell).toEqual(renderNumberCell("0 B")); + expect(disksCell).toMatchSnapshot(); }); it("renders sum of resources/stats cpus property", function() { @@ -77,9 +67,9 @@ describe("ServicesTable", function() { }); application.getResources.mockReturnValue({ cpus: application.cpus }); - var cpusCell = cpuRenderer(application); + var cpusCell = renderer.create(cpuRenderer(application)).toJSON(); - expect(cpusCell).toEqual(renderNumberCell(1)); + expect(cpusCell).toMatchSnapshot(); }); it("renders sum of resources/stats gpus property", function() { @@ -98,9 +88,9 @@ describe("ServicesTable", function() { }); application.getResources.mockReturnValue({ gpus: application.gpus }); - var gpusCell = gpuRenderer(application); + var gpusCell = renderer.create(gpuRenderer(application)).toJSON(); - expect(gpusCell).toEqual(renderNumberCell(application.gpus)); + expect(gpusCell).toMatchSnapshot(); }); it("renders sum of resources/stats mem property", function() { @@ -119,9 +109,9 @@ describe("ServicesTable", function() { }); application.getResources.mockReturnValue({ mem: application.mem }); - var memCell = memRenderer(application); + var memCell = renderer.create(memRenderer(application)).toJSON(); - expect(memCell).toEqual(renderNumberCell("2 GiB")); + expect(memCell).toMatchSnapshot(); }); it("renders sum of resources/stats disk property", function() { @@ -140,9 +130,9 @@ describe("ServicesTable", function() { }); application.getResources.mockReturnValue({ disk: application.disk }); - var diskCell = diskRenderer(application); + var diskCell = renderer.create(diskRenderer(application)).toJSON(); - expect(diskCell).toEqual(renderNumberCell("0 B")); + expect(diskCell).toMatchSnapshot(); }); }); diff --git a/plugins/services/src/js/containers/services/__tests__/__snapshots__/ServicesTableRenderers-test.js.snap b/plugins/services/src/js/containers/services/__tests__/__snapshots__/ServicesTableRenderers-test.js.snap index 91bef52170..87e58c3535 100644 --- a/plugins/services/src/js/containers/services/__tests__/__snapshots__/ServicesTableRenderers-test.js.snap +++ b/plugins/services/src/js/containers/services/__tests__/__snapshots__/ServicesTableRenderers-test.js.snap @@ -41,3 +41,83 @@ exports[`ServicesTable #renderRegions renders with one region 1`] = ` `; + +exports[`ServicesTable #renderStats renders resources/stats cpus property 1`] = ` + + + 1 + + +`; + +exports[`ServicesTable #renderStats renders resources/stats disk property 1`] = ` + + + 0 B + + +`; + +exports[`ServicesTable #renderStats renders resources/stats gpus property 1`] = ` + + + 1 + + +`; + +exports[`ServicesTable #renderStats renders resources/stats mem property 1`] = ` + + + 2 GiB + + +`; + +exports[`ServicesTable #renderStats renders sum of resources/stats cpus property 1`] = ` + + + 1 + + +`; + +exports[`ServicesTable #renderStats renders sum of resources/stats gpus property 1`] = ` + + + 1 + + +`; + +exports[`ServicesTable #renderStats renders sum of resources/stats mem property 1`] = ` + + + 2 GiB + + +`; + +exports[`ServicesTable #renderStats renders sum of resources/stats disk property 1`] = ` + + + 0 B + + +`; diff --git a/plugins/services/src/js/utils/DeclinedOffersUtil.d.ts b/plugins/services/src/js/utils/DeclinedOffersUtil.d.ts new file mode 100644 index 0000000000..0a61478b75 --- /dev/null +++ b/plugins/services/src/js/utils/DeclinedOffersUtil.d.ts @@ -0,0 +1,20 @@ +interface Queue { + app?: unknown; + pod?: unknown; + processedOffersSummary?: unknown; + lastUnusedOffers?: unknown; + since?: unknown; +} + +interface ItemWithQueue { + getQueue(): Queue | null; +} + +export default class DeclinedOffersUtil { + static getTimeWaiting(queue: Queue): string | null; + static displayDeclinedOffersWarning( + item: T + ): boolean; + getSummaryFromQueue(queue: Queue): unknown; + getOffersFromQueue(queue: Queue): unknown; +} diff --git a/plugins/services/src/js/utils/ServiceStatusIconUtil.ts b/plugins/services/src/js/utils/ServiceStatusIconUtil.ts new file mode 100644 index 0000000000..5112ec0dbe --- /dev/null +++ b/plugins/services/src/js/utils/ServiceStatusIconUtil.ts @@ -0,0 +1,59 @@ +import DateUtil from "#SRC/js/utils/DateUtil"; + +import Pod from "../structs/Pod"; +import Service from "../structs/Service"; +import ServiceTree from "../structs/ServiceTree"; +import DeclinedOffersUtil from "../utils/DeclinedOffersUtil"; + +interface ServiceQueue { + since: string | null; +} + +const isUnableToLaunch = (service: Service | Pod | ServiceTree) => { + const queue = service.getQueue() as ServiceQueue | null; + const UNABLE_TO_LAUNCH_TIMEOUT = 1000 * 60 * 30; + + if (queue == null) { + return false; + } + + return ( + Date.now() - (DateUtil.strToMs(queue.since) || 0) >= + UNABLE_TO_LAUNCH_TIMEOUT + ); +}; + +export function getStatusIconProps(service: Service | ServiceTree | Pod) { + const queue = service.getQueue() as ServiceQueue | null; + const isServiceTree = service instanceof ServiceTree; + const unableToLaunch = isUnableToLaunch(service); + const appsWithWarnings = isServiceTree + ? (service as ServiceTree) + .filterItems((item: any) => { + if (!(item instanceof ServiceTree)) { + return ( + DeclinedOffersUtil.displayDeclinedOffersWarning(item) || + isUnableToLaunch(item) + ); + } + + return false; + }) + .flattenItems() + .getItems().length + : null; + + return { + id: service.getId(), + timeWaiting: queue ? DeclinedOffersUtil.getTimeWaiting(queue) : null, + timeQueued: queue ? DateUtil.strToMs(queue.since) : null, + serviceStatus: service.getServiceStatus(), + isServiceTree, + isService: service instanceof Service, + displayDeclinedOffers: DeclinedOffersUtil.displayDeclinedOffersWarning( + service + ), + appsWithWarnings, + unableToLaunch + }; +}