diff --git a/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx b/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx index 854a6befbf..cde11b51fa 100644 --- a/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx +++ b/plugins/services/src/js/columns/ServicesTableStatusColumn.tsx @@ -10,6 +10,7 @@ import Pod from "../structs/Pod"; import Service from "../structs/Service"; import ServiceTree from "../structs/ServiceTree"; import ServiceTableUtil from "../utils/ServiceTableUtil"; +import { getStatusIconProps } from "../utils/ServiceStatusIconUtil"; function statusCountsToTooltipContent(counts: { total: number; @@ -29,67 +30,135 @@ function statusCountsToTooltipContent(counts: { }); } -export function statusRenderer(node: Service | ServiceTree): React.ReactNode { - return node instanceof ServiceTree - ? renderServiceTree(node) - : renderService(node); +//Incoming +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; } -function renderService(service: Service | Pod): React.ReactNode { - const status = service.getStatus(); - if (isNA(status)) { +interface TreeStatusColumnProps extends StatusColumnProps { + statusText: string; + totalCount: number; + priorityStatusCount: number; + tooltipContent: JSX.Element; +} + +export function statusRenderer( + service: Service | ServiceTree +): React.ReactNode { + const iconProps = getStatusIconProps(service); + + const props = { + statusText: service.getStatus(), + instancesCount: service.getInstancesCount(), + runningInstances: service.getRunningInstancesCount() + }; + let serviceTreeProps = {}; + if (service instanceof ServiceTree) { + const summary = service.getServiceTreeStatusSummary(); + const statusText = ServiceStatus.toCategoryLabel(summary.status); + const totalCount = summary.counts.total; + const priorityStatusCount = summary.counts.status[summary.status]; + const tooltipContent = statusCountsToTooltipContent(summary.counts); + serviceTreeProps = { + statusText, + totalCount, + priorityStatusCount, + tooltipContent + }; + } + + return service instanceof ServiceTree ? ( + + ) : ( + + ); +} + +//Master +function RenderService(props: StatusColumnProps): React.ReactNode { + //const status = service.getStatus(); + if (isNA(props.statusText)) { return null; } - const instancesCount = service.getInstancesCount(); + //const instancesCount = service.getInstancesCount(); return (
} /> - +
- +
); } -function renderServiceTree(service: ServiceTree): React.ReactNode { - const summary = service.getServiceTreeStatusSummary(); - const statusText = ServiceStatus.toCategoryLabel(summary.status); - if (isNA(statusText)) { +function RenderServiceTree(props: TreeStatusColumnProps): React.ReactNode { + if (isNA(props.statusText)) { return null; } - const totalCount = summary.counts.total; - const priorityStatusCount = summary.counts.status[summary.status]; return (
{statusCountsToTooltipContent(summary.counts)} - } + tooltipContent={{props.tooltipContent}} /> - {" "} - {totalCount > 1 ? ( + {" "} + {props.totalCount > 1 ? ( - ({priorityStatusCount} of {totalCount}) + ({props.priorityStatusCount} of {props.totalCount}) ) : null} 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 3c6961f23f..d27c689f64 100644 --- a/plugins/services/src/js/components/ServiceStatusIcon.tsx +++ b/plugins/services/src/js/components/ServiceStatusIcon.tsx @@ -13,14 +13,9 @@ import StringUtil from "#SRC/js/utils/StringUtil"; // @ts-ignore import DeclinedOffersUtil from "../utils/DeclinedOffersUtil"; import * as ServiceStatus 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 +30,42 @@ const getTooltipContent = (service: Service, content: JSX.Element) => { ); }; -type TreeNode = Service | ServiceTree; - class ServiceStatusIcon extends React.Component<{ showTooltip?: boolean; tooltipContent: React.ReactNode; - service: TreeNode; + id: string | number; + isService: boolean; + isServiceTree: boolean; + serviceStatus: any; + timeWaiting: string | null; + timeQueued: number | null; + appsWithWarnings: number | null; + displayDeclinedOffers: boolean; + unableToLaunch: boolean; }> { 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 +73,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,7 +91,6 @@ class ServiceStatusIcon extends React.Component<{ } getTooltip(content: JSX.Element) { - const { service } = this.props; let icon = ( ); + const { id, isService } = this.props; - 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}; } @@ -129,30 +118,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: StatusIcon) { @@ -177,26 +155,31 @@ class ServiceStatusIcon extends React.Component<{ } render() { - const { service } = this.props; - const iconState = ServiceStatus.toIcon(service.getServiceStatus()); + const { + serviceStatus, + isServiceTree, + displayDeclinedOffers, + unableToLaunch + } = this.props; + const iconState = ServiceStatus.toIcon(serviceStatus); // Catch other cases instead throwing a warning/error if (iconState == null) { 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 590f639596..11c09b2769 100644 --- a/plugins/services/src/js/components/ServiceStatusProgressBar.tsx +++ b/plugins/services/src/js/components/ServiceStatusProgressBar.tsx @@ -3,15 +3,14 @@ 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; } export const ServiceProgressBar = React.memo( @@ -48,19 +47,15 @@ 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 }; 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 2569735427..c699d71b21 100644 --- a/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js +++ b/plugins/services/src/js/components/__tests__/ServiceStatusProgressBar-test.js @@ -1,26 +1,108 @@ import React from "react"; -import renderer from "react-test-renderer"; - -import { ServiceProgressBar } from "../ServiceStatusProgressBar"; - -describe("ServiceProgressBar", () => { - for (const [runningInstances, instancesCount] of [ - [0, 1], - [0, 10], - [3, 4], - [10, 10] - ]) { - it(`displays a (${runningInstances} / ${instancesCount}) ProgressBar`, () => { - expect( - renderer - .create( - - ) - .toJSON() - ).toMatchSnapshot(); +import { shallow } from "enzyme"; + +import ServiceStatusProgressBar from "../ServiceStatusProgressBar"; + +const Tooltip = require("reactjs-components").Tooltip; + +const ProgressBar = require("#SRC/js/components/ProgressBar"); +const Application = require("../../structs/Application"); +const Pod = require("../../structs/Pod"); + +const service = new Application({ + instances: 1, + tasksRunning: 1, + tasksHealthy: 0, + tasksUnhealthy: 0, + tasksUnknown: 0, + tasksStaged: 0 +}); + +let thisInstance; + +describe("ServiceStatusProgressBar", function() { + beforeEach(function() { + 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( + + ); + + const progressBar = thisInstance.find(ProgressBar); + expect(progressBar).toBeTruthy(); + expect(progressBar.prop("total")).toBe(10); + expect(progressBar.prop("data")[0].value).toBe(4); + }); + }); + + describe("Tooltip", function() { + it("display Tooltip", function() { + const service = new Application({ + queue: { + overdue: true + } + }); + + thisInstance = shallow( + + ); + + expect(expect(thisInstance.find(Tooltip).exists())).toBeTruthy(); + }); + + it("get #getTooltipContent", function() { + const childrenContent = shallow( + thisInstance.instance().getTooltipContent() + ) + .find("WithI18n") + .props().values; + + expect(childrenContent).toMatchObject({ + 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/ServicesTable.js b/plugins/services/src/js/containers/services/ServicesTable.js index 5c9143caa9..b0e3a63942 100644 --- a/plugins/services/src/js/containers/services/ServicesTable.js +++ b/plugins/services/src/js/containers/services/ServicesTable.js @@ -23,7 +23,9 @@ import { } from "#SRC/js/core/MesosMasterRequest"; import container from "#SRC/js/container"; +import { findNestedPropertyInObject } from "#SRC/js/utils/Util"; import { isSDKService } from "../../utils/ServiceUtil"; +import ServiceTableUtil from "../../utils/ServiceTableUtil"; import { ServiceActionItem } from "../../constants/ServiceActionItem"; import ServiceActionLabels from "../../constants/ServiceActionLabels"; @@ -60,6 +62,7 @@ import { } from "../../columns/ServicesTableDiskColumn"; import { gpuRenderer, gpuSorter } from "../../columns/ServicesTableGPUColumn"; import { actionsRendererFactory } from "../../columns/ServicesTableActionsColumn"; +import { getStatusIconProps } from "../../utils/ServiceStatusIconUtil"; const DELETE = ServiceActionItem.DELETE; const EDIT = ServiceActionItem.EDIT; @@ -77,6 +80,62 @@ const METHODS_TO_BIND = [ "handleSortClick" ]; +const isServiceDataNew = (prevData, nextData) => { + const serviceDataCompareArr = nextData.map((nextServiceData, i) => { + if (!nextServiceData || !prevData[i]) { + return false; + } + + const sameStatus = + getStatusIconProps(prevData[i]).serviceStatus.key === + getStatusIconProps(nextServiceData).serviceStatus.key; + const sameVersion = + findNestedPropertyInObject( + ServiceTableUtil.getFormattedVersion(nextServiceData), + "displayVersion" + ) === + findNestedPropertyInObject( + ServiceTableUtil.getFormattedVersion(prevData[i]), + "displayVersion" + ); + const sameReg = + nextServiceData.getRegions().length === + [...new Set(nextServiceData.getRegions(), prevData[i].getRegions())] + .length; + const sameInstances = + nextServiceData.getInstancesCount() === prevData[i].getInstancesCount() || + nextServiceData.getRunningInstancesCount() === + prevData[i].getRunningInstancesCount(); + const sameCPU = + nextServiceData.getResources()["cpus"] === + prevData[i].getResources()["cpus"]; + const sameMem = + nextServiceData.getResources()["mem"] === + prevData[i].getResources()["mem"]; + const sameDisk = + nextServiceData.getResources()["disk"] === + prevData[i].getResources()["disk"]; + const sameGPU = + nextServiceData.getResources()["gpus"] === + prevData[i].getResources()["gpus"]; + + const isChanged = [ + sameStatus, + sameVersion, + sameReg, + sameInstances, + sameCPU, + sameMem, + sameDisk, + sameGPU + ].some(hasChange => hasChange === false); + + return isChanged; + }); + + return serviceDataCompareArr.some(isChanged => isChanged === true); +}; + class ServicesTable extends React.Component { constructor(props) { super(...arguments); @@ -108,6 +167,16 @@ class ServicesTable extends React.Component { CompositeState.enable(); } + shouldComponentUpdate(nextProps) { + const sameServiceCount = + nextProps.services.length === this.props.services.length; + + return ( + !sameServiceCount || + isServiceDataNew(this.props.services, nextProps.services) + ); + } + componentWillReceiveProps(nextProps) { this.regionRenderer = regionRendererFactory(nextProps.masterRegionName); diff --git a/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js b/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js new file mode 100644 index 0000000000..8b481ba588 --- /dev/null +++ b/plugins/services/src/js/containers/services/__tests__/ServicesTableRenderers-test.js @@ -0,0 +1,172 @@ +import { regionRenderer } from "../../../columns/ServicesTableRegionColumn"; +import { cpuRenderer } from "../../../columns/ServicesTableCPUColumn"; +import { memRenderer } from "../../../columns/ServicesTableMemColumn"; +import { diskRenderer } from "../../../columns/ServicesTableDiskColumn"; +import { gpuRenderer } from "../../../columns/ServicesTableGPUColumn"; + +// Explicitly mock for react-intl +jest.mock("../../../components/modals/ServiceActionDisabledModal"); + +/* eslint-disable no-unused-vars */ +const React = require("react"); +/* eslint-enable no-unused-vars */ +const renderer = require("react-test-renderer"); +const Application = require("../../../structs/Application"); + +describe("ServicesTable", function() { + const healthyService = new Application({ + healthChecks: [{ path: "", protocol: "HTTP" }], + cpus: 1, + gpus: 1, + instances: 1, + mem: 2048, + disk: 0, + tasksStaged: 0, + tasksRunning: 2, + tasksHealthy: 2, + tasksUnhealthy: 0 + }); + + describe("#renderStats", function() { + it("renders resources/stats cpus property", function() { + var cpusCell = renderer.create(cpuRenderer(healthyService)).toJSON(); + + expect(cpusCell).toMatchSnapshot(); + }); + + it("renders resources/stats gpus property", function() { + var gpusCell = renderer.create(gpuRenderer(healthyService)).toJSON(); + + expect(gpusCell).toMatchSnapshot(); + }); + + it("renders resources/stats mem property", function() { + var memCell = renderer.create(memRenderer(healthyService)).toJSON(); + + expect(memCell).toMatchSnapshot(); + }); + + it("renders resources/stats disk property", function() { + var disksCell = renderer.create(diskRenderer(healthyService)).toJSON(); + + expect(disksCell).toMatchSnapshot(); + }); + + it("renders sum of resources/stats cpus property", function() { + const application = new Application({ + healthChecks: [{ path: "", protocol: "HTTP" }], + cpus: 1, + instances: 2, + mem: 2048, + disk: 0, + tasksStaged: 0, + tasksRunning: 2, + tasksHealthy: 2, + tasksUnhealthy: 0, + getResources: jest.fn() + }); + + application.getResources.mockReturnValue({ cpus: application.cpus }); + var cpusCell = renderer.create(cpuRenderer(application)).toJSON(); + + expect(cpusCell).toMatchSnapshot(); + }); + + it("renders sum of resources/stats gpus property", function() { + const application = new Application({ + healthChecks: [{ path: "", protocol: "HTTP" }], + cpus: 1, + gpus: 1, + instances: 2, + mem: 2048, + disk: 0, + tasksStaged: 0, + tasksRunning: 2, + tasksHealthy: 2, + tasksUnhealthy: 0, + getResources: jest.fn() + }); + + application.getResources.mockReturnValue({ gpus: application.gpus }); + var gpusCell = renderer.create(gpuRenderer(application)).toJSON(); + + expect(gpusCell).toMatchSnapshot(); + }); + + it("renders sum of resources/stats mem property", function() { + const application = new Application({ + healthChecks: [{ path: "", protocol: "HTTP" }], + cpus: 1, + gpus: 1, + instances: 2, + mem: 2048, + disk: 0, + tasksStaged: 0, + tasksRunning: 2, + tasksHealthy: 2, + tasksUnhealthy: 0, + getResources: jest.fn() + }); + + application.getResources.mockReturnValue({ mem: application.mem }); + var memCell = renderer.create(memRenderer(application)).toJSON(); + + expect(memCell).toMatchSnapshot(); + }); + + it("renders sum of resources/stats disk property", function() { + const application = new Application({ + healthChecks: [{ path: "", protocol: "HTTP" }], + cpus: 1, + gpus: 1, + instances: 2, + mem: 2048, + disk: 0, + tasksStaged: 0, + tasksRunning: 2, + tasksHealthy: 2, + tasksUnhealthy: 0, + getResources: jest.fn() + }); + + application.getResources.mockReturnValue({ disk: application.disk }); + var diskCell = renderer.create(diskRenderer(application)).toJSON(); + + expect(diskCell).toMatchSnapshot(); + }); + }); + + describe("#renderRegions", function() { + const renderRegions = regionRenderer; + let mockService; + beforeEach(function() { + mockService = { + getRegions: jest.fn() + }; + }); + + it("renders with no regions", function() { + mockService.getRegions.mockReturnValue([]); + + expect( + renderer.create(renderRegions(mockService)).toJSON() + ).toMatchSnapshot(); + }); + + it("renders with one region", function() { + mockService.getRegions.mockReturnValue(["aws/eu-central-1"]); + + expect( + renderer.create(renderRegions(mockService)).toJSON() + ).toMatchSnapshot(); + }); + + it("renders with multiple regions", function() { + mockService.getRegions.mockReturnValue(["aws/eu-central-1", "dc-east"]); + + expect( + renderer.create(renderRegions(mockService)).toJSON() + ).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 new file mode 100644 index 0000000000..87e58c3535 --- /dev/null +++ b/plugins/services/src/js/containers/services/__tests__/__snapshots__/ServicesTableRenderers-test.js.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServicesTable #renderRegions renders with multiple regions 1`] = ` +
+ + aws/eu-central-1, dc-east + +
+`; + +exports[`ServicesTable #renderRegions renders with no regions 1`] = ` +
+ + N/A + +
+`; + +exports[`ServicesTable #renderRegions renders with one region 1`] = ` +
+ + aws/eu-central-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 + }; +}