@@ -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
+ };
+}