mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 15:24:52 +09:00
Merge pull request #827 from Cooldude2606/feature/startedAtMs
Feature/started at ms
This commit is contained in:
18 files changed
+182
-39
No files matched your search
@@ -96,6 +96,7 @@ export default class Controller {
|
||||
|
||||
devMiddleware: any | null = null;
|
||||
|
||||
startedAtMs = Date.now();
|
||||
autosaveInterval?: ReturnType<typeof setInterval>;
|
||||
systemMetricsInterval?: ReturnType<typeof setInterval>;
|
||||
|
||||
@@ -397,6 +398,7 @@ export default class Controller {
|
||||
this.onSystemMetricsIntervalChanged();
|
||||
|
||||
logger.info("Started controller");
|
||||
this.startedAtMs = Date.now();
|
||||
this._state = "running";
|
||||
}
|
||||
|
||||
@@ -553,7 +555,7 @@ export default class Controller {
|
||||
requests.push(hostConnection.send(new lib.SystemInfoRequest()));
|
||||
}
|
||||
const restartRequired = await this.checkRestartRequired();
|
||||
requests.push(lib.gatherSystemInfo("controller", this.canRestart, restartRequired));
|
||||
requests.push(lib.gatherSystemInfo("controller", this.canRestart, restartRequired, this.startedAtMs));
|
||||
const newMetrics = await Promise.all(requests);
|
||||
for (const metric of newMetrics) {
|
||||
this.systems.set(metric);
|
||||
@@ -663,6 +665,12 @@ export default class Controller {
|
||||
if (!json.restartRequired) { // Added in 2.0.0.alpha.21
|
||||
json.restartRequired = false;
|
||||
}
|
||||
if (json.systemStartedAtMs === undefined) { // Added in 2.0.0.alpha.23
|
||||
json.systemStartedAtMs = Date.now();
|
||||
}
|
||||
if (json.processStartedAtMs === undefined) { // Added in 2.0.0.alpha.23
|
||||
json.processStartedAtMs = Date.now();
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,6 +207,11 @@ export default class HostConnection extends BaseConnection {
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = request.status;
|
||||
// If the instance has transitioned to running then updated the started at time
|
||||
if (request.status === "running" && prev === "starting") {
|
||||
instance.startedAtMs = Date.now();
|
||||
}
|
||||
|
||||
instance.gamePort = request.gamePort;
|
||||
instance.factorioVersion = request.factorioVersion;
|
||||
this._controller.instances.set(instance);
|
||||
@@ -246,6 +251,9 @@ export default class HostConnection extends BaseConnection {
|
||||
controllerInstance.status = instanceData.status;
|
||||
controllerInstance.gamePort = instanceData.gamePort;
|
||||
controllerInstance.factorioVersion = instanceData.factorioVersion;
|
||||
if (instanceData.status === "running" && prev === "starting") {
|
||||
controllerInstance.startedAtMs = Date.now();
|
||||
}
|
||||
instanceUpdates.push(controllerInstance);
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
await lib.invokeHook(
|
||||
@@ -261,6 +269,7 @@ export default class HostConnection extends BaseConnection {
|
||||
instanceData.status,
|
||||
instanceData.gamePort,
|
||||
instanceData.factorioVersion,
|
||||
0,
|
||||
Date.now(),
|
||||
);
|
||||
instanceUpdates.push(newInstance);
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class InstanceInfo {
|
||||
public status: lib.InstanceStatus,
|
||||
public gamePort?: number,
|
||||
public factorioVersion?: lib.TargetVersion,
|
||||
public startedAtMs = 0,
|
||||
public updatedAtMs = 0,
|
||||
) {
|
||||
this.config = config;
|
||||
@@ -22,6 +23,7 @@ export default class InstanceInfo {
|
||||
"config": lib.InstanceConfig.jsonSchema,
|
||||
"status": lib.InstanceStatus,
|
||||
"gamePort": Type.Optional(Type.Number()),
|
||||
"startedAtMs": Type.Optional(Type.Number()),
|
||||
"updatedAtMs": Type.Optional(Type.Number()),
|
||||
"factorioVersion": Type.Optional(lib.TargetVersionSchema),
|
||||
});
|
||||
@@ -32,6 +34,7 @@ export default class InstanceInfo {
|
||||
json.status,
|
||||
json.gamePort,
|
||||
json.factorioVersion,
|
||||
json.startedAtMs,
|
||||
json.updatedAtMs,
|
||||
);
|
||||
}
|
||||
@@ -44,6 +47,7 @@ export default class InstanceInfo {
|
||||
this.gamePort,
|
||||
this.status,
|
||||
this.factorioVersion,
|
||||
this.startedAtMs,
|
||||
this.updatedAtMs,
|
||||
this.config.get("instance.exclude_from_start_all"),
|
||||
);
|
||||
|
||||
@@ -291,6 +291,7 @@ async function startHost() {
|
||||
});
|
||||
|
||||
await hostConnector.connect();
|
||||
host.startedAtMs = Date.now();
|
||||
logger.info("Started host");
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +278,8 @@ export default class Host extends lib.Link {
|
||||
_disconnecting = false;
|
||||
_shuttingDown = false;
|
||||
|
||||
startedAtMs = Date.now();
|
||||
|
||||
static instanceConfigWarning = {
|
||||
"_warning": "Changes to this file will be overwritten by the controller's copy.",
|
||||
};
|
||||
@@ -807,7 +809,7 @@ export default class Host extends lib.Link {
|
||||
|
||||
async handleSystemInfoRequest() {
|
||||
const restartRequired = await this.checkRestartRequired();
|
||||
return lib.gatherSystemInfo(this.config.get("host.id"), this.canRestart, restartRequired);
|
||||
return lib.gatherSystemInfo(this.config.get("host.id"), this.canRestart, restartRequired, this.startedAtMs);
|
||||
}
|
||||
|
||||
async checkRestartRequired() {
|
||||
|
||||
@@ -260,6 +260,8 @@ export class SystemInfo {
|
||||
public diskAvailable: number,
|
||||
public canRestart: boolean,
|
||||
public restartRequired: boolean,
|
||||
public systemStartedAtMs: number,
|
||||
public processStartedAtMs: number,
|
||||
/** Millisecond Unix timestamp this entry was last updated at */
|
||||
public updatedAtMs: number,
|
||||
public isDeleted: boolean,
|
||||
@@ -279,6 +281,8 @@ export class SystemInfo {
|
||||
"diskAvailable": Type.Number(),
|
||||
"canRestart": Type.Boolean(),
|
||||
"restartRequired": Type.Boolean(),
|
||||
"systemStartedAtMs": Type.Number(),
|
||||
"processStartedAtMs": Type.Number(),
|
||||
"updatedAtMs": Type.Number(),
|
||||
"isDeleted": Type.Boolean(),
|
||||
});
|
||||
@@ -298,6 +302,8 @@ export class SystemInfo {
|
||||
json.diskAvailable,
|
||||
json.canRestart,
|
||||
json.restartRequired,
|
||||
json.systemStartedAtMs,
|
||||
json.processStartedAtMs,
|
||||
json.updatedAtMs,
|
||||
json.isDeleted,
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ export class InstanceDetails {
|
||||
public gamePort: number | undefined,
|
||||
public status: InstanceStatus,
|
||||
public factorioVersion: TargetVersion | undefined,
|
||||
public startedAtMs = 0,
|
||||
/** Millisecond Unix timestamp this entry was last updated at */
|
||||
public updatedAtMs = 0,
|
||||
public excludeFromStartAll = false,
|
||||
@@ -59,6 +60,7 @@ export class InstanceDetails {
|
||||
"creating_save", "exporting_data", "deleted",
|
||||
]),
|
||||
"factorioVersion": Type.Optional(TargetVersionSchema),
|
||||
"startedAtMs": Type.Optional(Type.Number()),
|
||||
"updatedAtMs": Type.Optional(Type.Number()),
|
||||
"excludeFromStartAll": Type.Optional(Type.Boolean()),
|
||||
});
|
||||
@@ -71,6 +73,7 @@ export class InstanceDetails {
|
||||
json.gamePort,
|
||||
json.status,
|
||||
json.factorioVersion,
|
||||
json.startedAtMs,
|
||||
json.updatedAtMs,
|
||||
json.excludeFromStartAll,
|
||||
);
|
||||
|
||||
@@ -134,7 +134,12 @@ function minZip<T>(a: T[], b: T[]): [T, T][] {
|
||||
|
||||
let previousTotalCpuMs: number[] = [];
|
||||
let previousIdleCpuMs: number[] = [];
|
||||
export async function gatherSystemInfo(id: SystemInfo["id"], canRestart: boolean, restartRequired: boolean) {
|
||||
export async function gatherSystemInfo(
|
||||
id: SystemInfo["id"],
|
||||
canRestart: boolean,
|
||||
restartRequired: boolean,
|
||||
startedAtMs: number
|
||||
) {
|
||||
const cpus = os.cpus();
|
||||
const currentTotalCpuMs = cpus.map(({ times }) => times.user + times.idle + times.irq + times.nice + times.sys);
|
||||
const currentIdleCpuMs = cpus.map(({ times }) => times.idle);
|
||||
@@ -152,6 +157,7 @@ export async function gatherSystemInfo(id: SystemInfo["id"], canRestart: boolean
|
||||
diskAvailable = stats.bavail * stats.bsize;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
return new SystemInfo(
|
||||
id,
|
||||
os.hostname(),
|
||||
@@ -166,7 +172,9 @@ export async function gatherSystemInfo(id: SystemInfo["id"], canRestart: boolean
|
||||
diskAvailable,
|
||||
canRestart,
|
||||
restartRequired,
|
||||
Date.now(),
|
||||
now - (os.uptime() * 1000),
|
||||
startedAtMs,
|
||||
now,
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import PluginExtra from "./PluginExtra";
|
||||
import LogConsole, { SelectMaxLogLevel } from "./LogConsole";
|
||||
import {
|
||||
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
|
||||
MetricDiskUsed, MetricDiskRatio,
|
||||
MetricDiskUsed, MetricDiskRatio, MetricRelativeDate,
|
||||
} from "./system_metrics";
|
||||
import { useAccount } from "../model/account";
|
||||
import { useSystems } from "../model/system";
|
||||
@@ -101,6 +101,12 @@ export default function ControllerPage() {
|
||||
<Descriptions.Item label="Node.js">{system?.node}</Descriptions.Item>
|
||||
<Descriptions.Item label="OS Kernel">{system?.kernel}</Descriptions.Item>
|
||||
<Descriptions.Item label="Machine">{system?.machine}</Descriptions.Item>
|
||||
<Descriptions.Item label="System Uptime">
|
||||
<MetricRelativeDate timeMs={system?.systemStartedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Process Uptime">
|
||||
<MetricRelativeDate timeMs={system?.processStartedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Hostname" span={2}>{system?.hostname}</Descriptions.Item>
|
||||
<Descriptions.Item label="CPU Model" span={2}>{system?.cpuModel}</Descriptions.Item>
|
||||
<Descriptions.Item label="CPU Usage"><MetricCpuRatio system={system} /></Descriptions.Item>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Tag } from "antd";
|
||||
import React from "react";
|
||||
|
||||
import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const statusOptions = {
|
||||
connected: <Tag color="#389e0d" icon={<CheckCircleOutlined/>} >Connected</Tag>,
|
||||
disconnected: <Tag color="#cf1322" icon={<CloseCircleOutlined/>} >Disconnected</Tag>,
|
||||
};
|
||||
|
||||
type HostStatusTagProps = {
|
||||
connected: boolean;
|
||||
};
|
||||
|
||||
export default function HostStatusTag(props: HostStatusTagProps) {
|
||||
return props.connected ? statusOptions.connected : statusOptions.disconnected;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Descriptions, Spin, Tag, Typography, Button, Space, Modal, Popconfirm, Flex, Tooltip } from "antd";
|
||||
import { Descriptions, Spin, Typography, Button, Space, Popconfirm, Flex, Tooltip } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as lib from "@clusterio/lib";
|
||||
import notify, { notifyErrorHandler } from "../util/notify";
|
||||
import ControlContext from "./ControlContext";
|
||||
import HostConfigTree from "./HostConfigTree";
|
||||
import HostStatusTag from "./HostStatusTag";
|
||||
import InstanceList from "./InstanceList";
|
||||
import LogConsole, { SelectMaxLogLevel } from "./LogConsole";
|
||||
import { useAccount } from "../model/account";
|
||||
@@ -17,7 +18,7 @@ import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import {
|
||||
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
|
||||
MetricDiskUsed, MetricDiskRatio,
|
||||
MetricDiskUsed, MetricDiskRatio, MetricRelativeDate,
|
||||
} from "./system_metrics";
|
||||
import { formatTimestamp } from "../util/time_format";
|
||||
import { useSystems } from "../model/system";
|
||||
@@ -134,21 +135,22 @@ export default function HostViewPage() {
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
title={host.name || String(hostId)}
|
||||
status={<HostStatusTag connected={host["connected"]}/>}
|
||||
extra={hostButtons}
|
||||
/>
|
||||
<Descriptions bordered size="small" column={{ xs: 1, md: 2, lg: 2, xl: 2, xxl: 2 }}>
|
||||
<Descriptions.Item label="Name">{host["name"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Connected">
|
||||
<Tag color={host["connected"] ? "#389e0d" : "#cf1322"}>
|
||||
{host["connected"] ? "Connected" : "Disconnected"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{host.version}</Descriptions.Item>
|
||||
<Descriptions.Item label="Node.js">{system?.node}</Descriptions.Item>
|
||||
<Descriptions.Item label="OS Kernel">{system?.kernel}</Descriptions.Item>
|
||||
<Descriptions.Item label="Machine">{system?.machine}</Descriptions.Item>
|
||||
<Descriptions.Item label="Hostname" span={2}>{system?.hostname}</Descriptions.Item>
|
||||
<Descriptions.Item label="Connected From" span={2}>{host.remoteAddress}</Descriptions.Item>
|
||||
<Descriptions.Item label="System Uptime">
|
||||
<MetricRelativeDate timeMs={system?.systemStartedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Process Uptime">
|
||||
<MetricRelativeDate timeMs={system?.processStartedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Hostname">{system?.hostname}</Descriptions.Item>
|
||||
<Descriptions.Item label="Connected From">{host.remoteAddress}</Descriptions.Item>
|
||||
<Descriptions.Item label="CPU Model" span={2}>{system?.cpuModel}</Descriptions.Item>
|
||||
<Descriptions.Item label="CPU Usage"><MetricCpuRatio system={system} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Cores"><MetricCpuUsed system={system} /></Descriptions.Item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useRef, useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Flex, Form, InputNumber, Modal, Progress, Space, Switch, Table, Tag, Tooltip, Typography } from "antd";
|
||||
import { Button, Flex, Form, InputNumber, Modal, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import { ExclamationCircleOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
@@ -11,6 +11,7 @@ import ControlContext from "./ControlContext";
|
||||
import PageHeader from "./PageHeader";
|
||||
import PageLayout from "./PageLayout";
|
||||
import PluginExtra from "./PluginExtra";
|
||||
import HostStatusTag from "./HostStatusTag";
|
||||
import {
|
||||
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
|
||||
MetricDiskUsed, MetricDiskRatio,
|
||||
@@ -287,11 +288,7 @@ export default function HostsPage() {
|
||||
title: "Connected",
|
||||
key: "connected",
|
||||
render: (_, host) => <>
|
||||
<Tag
|
||||
color={host.connected ? "#389e0d" : "#cf1322"}
|
||||
>
|
||||
{host.connected ? "Connected" : "Disconnected"}
|
||||
</Tag>
|
||||
<HostStatusTag connected={host["connected"]}/>
|
||||
<RestartRequired system={systems.get(host.id)}/>
|
||||
</>,
|
||||
sorter: (a, b) => Number(a.connected) - Number(b.connected),
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { Tag } from "antd";
|
||||
import React from "react";
|
||||
|
||||
export const statusColors = {
|
||||
unknown: "#8c8c8c",
|
||||
unassigned: "#eb2f96",
|
||||
stopped: "#cf1322",
|
||||
starting: "#ad8b00",
|
||||
running: "#389e0d",
|
||||
stopping: "#d48806",
|
||||
creating_save: "#096dd9",
|
||||
exporting_data: "#08979c",
|
||||
deleted: "#262626",
|
||||
import {
|
||||
QuestionCircleOutlined, ClockCircleOutlined, StopOutlined, LoadingOutlined,
|
||||
CheckCircleOutlined, SaveOutlined, ExportOutlined, DeleteOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export const statusOptions = {
|
||||
unknown: <Tag color="#8c8c8c" icon={<QuestionCircleOutlined />}>Unknown</Tag>,
|
||||
unassigned: <Tag color="#eb2f96" icon={<ClockCircleOutlined />}>Unassigned</Tag>,
|
||||
stopped: <Tag color="#cf1322" icon={<StopOutlined />}>Stopped</Tag>,
|
||||
starting: <Tag color="#ad8b00" icon={<LoadingOutlined />}>Starting</Tag>,
|
||||
running: <Tag color="#389e0d" icon={<CheckCircleOutlined />}>Running</Tag>,
|
||||
stopping: <Tag color="#d48806" icon={<LoadingOutlined />}>Stopping</Tag>,
|
||||
creating_save: <Tag color="#096dd9" icon={<SaveOutlined />}>Creating Save</Tag>,
|
||||
exporting_data: <Tag color="#08979c" icon={<ExportOutlined />}>Exporting Data</Tag>,
|
||||
deleted: <Tag color="#262626" icon={<DeleteOutlined />}>Deleted</Tag>,
|
||||
};
|
||||
|
||||
type InstanceStatusTag = {
|
||||
status: keyof typeof statusColors;
|
||||
status: keyof typeof statusOptions;
|
||||
};
|
||||
|
||||
export default function InstanceStatusTag(props: InstanceStatusTag) {
|
||||
return <Tag color={statusColors[props.status]}>
|
||||
{props.status.replace("_", " ")}
|
||||
</Tag>;
|
||||
return statusOptions[props.status];
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { useHost } from "../model/host";
|
||||
import InstanceStatusTag from "./InstanceStatusTag";
|
||||
import Link from "./Link";
|
||||
import { instancePublicAddress } from "../util/instance";
|
||||
import { MetricRelativeDate } from "./system_metrics";
|
||||
|
||||
type MenuItem = Required<MenuProps>["items"][number];
|
||||
const { Title } = Typography;
|
||||
@@ -41,6 +42,7 @@ function InstanceDescription(props: InstanceDescriptionProps) {
|
||||
return <Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
column={{ xs: 1, md: 3, lg: 3, xl: 3, xxl: 3 }}
|
||||
>
|
||||
<Descriptions.Item label="Host">
|
||||
{!assigned
|
||||
@@ -64,7 +66,9 @@ function InstanceDescription(props: InstanceDescriptionProps) {
|
||||
<Descriptions.Item label="Version">
|
||||
{instance.factorioVersion ?? "unknown"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Status"><InstanceStatusTag status={instance.status!} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Last Start Time">
|
||||
<MetricRelativeDate timeMs={instance.startedAtMs}/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>;
|
||||
}
|
||||
|
||||
@@ -97,7 +101,7 @@ function InstanceButtons(props: { instance: lib.InstanceDetails }) {
|
||||
instanceButtonMenuItems.push({ type: "divider" });
|
||||
}
|
||||
instanceButtonMenuItems.push({
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance.status!),
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance.status),
|
||||
danger: true,
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
@@ -208,6 +212,7 @@ export default function InstanceViewPage() {
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
title={instance.name ?? ""}
|
||||
status={<InstanceStatusTag status={instance.status} />}
|
||||
extra={<InstanceButtons instance={instance} />}
|
||||
/>
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
|
||||
@@ -5,6 +5,7 @@ const { Text } = Typography;
|
||||
type PageHeaderProps = {
|
||||
title: string | React.ReactElement;
|
||||
subTitle?: string | React.ReactElement;
|
||||
status?: React.JSX.Element;
|
||||
extra?: React.JSX.Element;
|
||||
};
|
||||
|
||||
@@ -13,6 +14,7 @@ export default function PageHeader(props: PageHeaderProps): React.JSX.Element {
|
||||
<Space>
|
||||
<h2 className="page-header-title">{props.title}</h2>
|
||||
{props.subTitle && <Text type="secondary">{props.subTitle}</Text>}
|
||||
{props.status ?? null}
|
||||
</Space>
|
||||
{props.extra && <div className="page-header-extra">{props.extra}</div>}
|
||||
</div>;
|
||||
|
||||
@@ -63,3 +63,52 @@ export function RestartRequired(props: { system?: lib.SystemInfo }) {
|
||||
<ReloadOutlined style={{ color: "yellow" }}/>
|
||||
</Tooltip>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable duration between two dates.
|
||||
*
|
||||
* The output includes up to two most significant time units and omits
|
||||
* leading zero units. Supported units are weeks, days, hours, minutes,
|
||||
* and seconds.
|
||||
*
|
||||
* Examples: "1w 2d", "2d 3h", "1h 2m", "45s"
|
||||
*
|
||||
* @param date1 - The starting date.
|
||||
* @param date2 - The ending date.
|
||||
* @returns A duration string (e.g. "2d 3h").
|
||||
*/
|
||||
function humanAbsTimeDiff(date1: number, date2 = 0) {
|
||||
let diffMs = Math.abs(date2 - date1);
|
||||
const totalSeconds = Math.floor(diffMs / 1000);
|
||||
|
||||
const weeks = Math.floor(totalSeconds / 604800); // 7 * 24 * 60 * 60
|
||||
const days = Math.floor((totalSeconds % 604800) / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
if (weeks > 0) {
|
||||
return days > 0 ? `${weeks}w ${days}d` : `${weeks}w`;
|
||||
}
|
||||
|
||||
if (days > 0) {
|
||||
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
}
|
||||
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
export function MetricRelativeDate(props: { timeMs?: number }) {
|
||||
if (!props.timeMs || props.timeMs === 0) {
|
||||
return "N/A";
|
||||
}
|
||||
return `${new Date(props.timeMs).toLocaleString()} (${humanAbsTimeDiff(props.timeMs, Date.now())})`;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export { default as LogConsole } from "./components/LogConsole";
|
||||
export { default as AssignInstanceModal } from "./components/AssignInstanceModal";
|
||||
export { default as InstanceList } from "./components/InstanceList";
|
||||
export { default as InstanceRcon } from "./components/InstanceRcon";
|
||||
export { default as InstanceStatusTag, statusColors } from "./components/InstanceStatusTag";
|
||||
export { default as InstanceStatusTag } from "./components/InstanceStatusTag";
|
||||
export { default as InstanceControlButton, InstanceControlButtonPermissions } from "./components/InstanceControlButton";
|
||||
|
||||
export { default as PageHeader } from "./components/PageHeader";
|
||||
|
||||
@@ -315,9 +315,29 @@ describe("controller/src/Controller", function() {
|
||||
assert.equal(result[1].restartRequired, true);
|
||||
assert.equal(result[2].restartRequired, false);
|
||||
});
|
||||
it("migrates 'systemStartedAtMs' from undefined to Date.now", function() { // Alpha 23
|
||||
const result = Controller.migrateSystems([
|
||||
{}, { systemStartedAtMs: 0 }, { systemStartedAtMs: 500 },
|
||||
]);
|
||||
const now = Date.now();
|
||||
const r0 = result[0].systemStartedAtMs;
|
||||
assert.ok(r0 > now - 100 && r0 <= now);
|
||||
assert.equal(result[1].systemStartedAtMs, 0);
|
||||
assert.equal(result[2].systemStartedAtMs, 500);
|
||||
});
|
||||
it("migrates 'processStartedAtMs' from undefined to Date.now", function() { // Alpha 23
|
||||
const result = Controller.migrateSystems([
|
||||
{}, { processStartedAtMs: 0 }, { processStartedAtMs: 500 },
|
||||
]);
|
||||
const now = Date.now();
|
||||
const r0 = result[0].processStartedAtMs;
|
||||
assert.ok(r0 > now - 100 && r0 <= now);
|
||||
assert.equal(result[1].processStartedAtMs, 0);
|
||||
assert.equal(result[2].processStartedAtMs, 500);
|
||||
});
|
||||
it("does nothing for upto date data", function() {
|
||||
const systemInfo = new SystemInfo(
|
||||
1, "name", "n", "k", "m", "cm", "cr", "mc", "ma", "dc", "da", false, false, 0, false
|
||||
1, "name", "n", "k", "m", "cm", "cr", "mc", "ma", "dc", "da", false, false, 0, 0, 0, false
|
||||
);
|
||||
const jsonString = JSON.stringify(systemInfo);
|
||||
const systemInfoJson = JSON.parse(jsonString);
|
||||
|
||||
Reference in new issue
Block a user