Add AboutModal

This commit is contained in:
Cooldude2606 committed 2026-06-10 22:35:00 +01:00
1 parent f7d3516c24
commit 0e36df8ae3
4 files changed
+258 -51

No files matched your search

@@ -0,0 +1,188 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import { Modal, Button, Space, Typography, message, Row, Col, Card } from "antd";
import { ReloadOutlined, CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { ControlContext, useAccount, useHosts, useInstances, useUsers } from "@clusterio/web_ui";
import { PluginWebApi } from "@clusterio/lib";
import webUiPackage from "../../package.json";
const { Text } = Typography;
function formatOS(os?: string) {
switch (os) {
case "win32": return "Windows";
case "darwin": return "macOS";
case "linux": return "Linux";
default: return os;
}
}
export default function AboutModal({ open, onClose, onOpenChangelog }: any) {
const control = useContext(ControlContext);
const account = useAccount();
const [hosts] = useHosts();
const [instances] = useInstances();
const [users] = useUsers();
const [pluginList, setPluginList] = useState<PluginWebApi[]>([]);
useEffect(() => {
(async () => {
let response = await fetch(`${webRoot}api/plugins`);
if (response.ok) {
setPluginList(await response.json());
}
})();
}, [webRoot]);
const browserInfo = useMemo(() => ({
userAgent: navigator.userAgent,
platform: navigator.platform,
}), []);
const debugText = useMemo(() => {
const permissions = new Set(
account.roles.flatMap(role => role.permissions)
);
const date = new Date();
const lines: string[] = [
"Clusterio Debug Info",
`- Timestamp: ${date.toISOString()}`,
`- Local Time: ${date.toString()}`,
"",
"Build",
`- Version: ${webUiPackage.version}`,
`- Build Date: ${process.env.BUILD_DATE}`,
`- Commit Hash: ${process.env.COMMIT_HASH}`,
`- Environment: ${process.env.BUILD_ENV}`,
`- System: ${process.env.BUILD_OS}`,
"",
"Runtime",
`- Platform: ${browserInfo.platform}`,
`- User Agent: ${browserInfo.userAgent}`,
`- Screen: ${window.screen.width}x${window.screen.height}`,
`- Viewport: ${window.innerWidth}x${window.innerHeight}`,
"",
"Cluster",
`- Url: ${new URL(webRoot, window.location.origin)}`,
`- Hosts: ${hosts.size}`,
`- Instances: ${instances.size}`,
`- Plugins: ${pluginList.length}`,
`- Users: ${users.size}`,
"",
"Account",
`- Name: ${account.name}`,
`- Roles: ${account.roles.map(r => r.name).sort().join(", ") || "None"}`,
`- Permissions: ${permissions.size}`,
"",
"Permission List",
...[...permissions].sort().map(p => `- ${p}`),
"",
];
lines.push("Plugin List");
for (let plugin of pluginList) {
lines.push(`- ${plugin.name}`);
lines.push(` Version: ${plugin.version ?? "unknown"}`);
lines.push(` Loaded: ${plugin.loaded ? "yes" : "no"}`);
lines.push(` Controller: ${plugin.enabled ? "enabled" : "disabled"}`);
lines.push(` Web: ${control.plugins.get(plugin.name) ? "enabled" : "disabled"}`);
if (plugin.npmPackage) {
lines.push(` Package: ${plugin.npmPackage}`);
}
}
return lines.join("\n");
}, [account, hosts, instances, users, pluginList, browserInfo]);
async function copyDebug() {
await navigator.clipboard.writeText(debugText);
message.success("Copied debug info");
}
function forceRefresh() {
window.location.reload();
}
return <Modal open={open} onCancel={onClose} footer={null} title="About Clusterio" width={800}>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<Row gutter={[24, 16]}>
<Col xs={24} md={12}>
<Card title="Build Info">
<Space direction="vertical">
<Text>Version: {`${webUiPackage.version} (${process.env.COMMIT_HASH})`}</Text>
<Text>Date: {new Date(process.env.BUILD_DATE ?? "").toLocaleString()}</Text>
<Text>Environment: {process.env.BUILD_ENV}</Text>
<Text>System: {formatOS(process.env.BUILD_OS)}</Text>
</Space>
</Card>
</Col>
<Col xs={24} md={12}>
<Card title="Cluster Info">
<Space direction="vertical">
<Text>Hosts: {hosts.size}</Text>
<Text>Instances: {instances.size}</Text>
<Text>Plugins: {pluginList.length}</Text>
<Text>Users: {users.size}</Text>
</Space>
</Card>
</Col>
</Row>
<Row>
<Col span={24}>
<Space style={{ width: "100%", justifyContent: "space-between" }} wrap>
<Space wrap>
<Button icon={<InfoCircleOutlined />} onClick={onOpenChangelog}>
Changelog
</Button>
<Button
target="_blank"
href="https://github.com/clusterio/clusterio/issues"
icon={<span style={{ display: "flex", alignItems: "center" }}>
<img
src="https://github.githubassets.com/favicons/favicon-dark.svg"
style={{ width: 16 }}
/>
</span>}
>
GitHub Issues
</Button>
<Button
target="_blank"
href="https://discord.gg/mzAsgnm"
icon={<span style={{ display: "flex", alignItems: "center" }}>
<img
// eslint-disable-next-line max-len
src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d7f4ef6498ac018f2c55_Symbol.svg"
style={{ width: 16}}
/>
</span>}
>
Support Server
</Button>
</Space>
<Space wrap>
<Button icon={<ReloadOutlined />} onClick={forceRefresh}>
Reload
</Button>
<Button icon={<CopyOutlined />} onClick={copyDebug} type="primary">
Copy Debug
</Button>
</Space>
</Space>
</Col>
</Row>
</Space>
</Modal>;
}
@@ -1,61 +1,26 @@
import React, { useState, ReactNode } from "react";
import { Modal, Tooltip } from "antd";
import React from "react";
import { Modal } from "antd";
import ReactMarkdown from "react-markdown";
import webUiPackage from "../../package.json";
import changelog from "../../../../CHANGELOG.md";
const cleanedChangelog = changelog
.replace(/<!--[\s\S]*?-->/g, "") // remove comments
.replace(/^#\s+Changelog\s*\n?/i, ""); // remove top-level title
type Props = {
/** Optional tooltip text */
tooltip?: string;
/** Optional class for the clickable wrapper */
className?: string;
/** Custom clickable content */
children?: ReactNode;
};
/**
* Displays a clickable element which opens a modal containing the changelog.
* Defaults to showing the current version if no children are provided.
* Displays a modal element which contains the change log
*/
export default function ChangeLogModal({
tooltip = "View changelog",
className = "site-version",
children = webUiPackage.version,
}: Props) {
const [open, setOpen] = useState(false);
return (
<>
<Tooltip
title={tooltip}
placement="bottom"
align={{ offset: [0, -10] }}
>
<span
className={className}
style={{ cursor: "pointer" }}
onClick={() => setOpen(true)}
>
{children}
</span>
</Tooltip>
<Modal
title="Changelog"
open={open}
onCancel={() => setOpen(false)}
footer={null}
width={800}
>
<ReactMarkdown>
{cleanedChangelog}
</ReactMarkdown>
</Modal>
</>
);
export default function ChangeLogModal({ open, onClose }: any) {
return <Modal
title="Changelog"
open={open}
onCancel={onClose}
footer={null}
width={800}
>
<ReactMarkdown>
{cleanedChangelog}
</ReactMarkdown>
</Modal>;
}
+40 -1
View File
@@ -7,11 +7,13 @@ import ErrorBoundary from "./ErrorBoundary";
import ErrorPage from "./ErrorPage";
import ControlContext from "./ControlContext";
import ChangeLogModal from "./ChangeLogModal";
import AboutModal from "./AboutModal";
import { pages } from "../pages";
import { saveJson } from "../util/save_file";
import { useAccount } from "../model/account";
import { DraggingContext } from "../model/is_dragging";
import webUiPackage from "../../package.json";
import { ControlConfig } from "@clusterio/lib";
@@ -44,6 +46,9 @@ export default function SiteLayout() {
let plugins = useContext(ControlContext).plugins;
const control = useContext(ControlContext);
const [dragging, setDragging] = useState(0);
const [aboutOpen, setAboutOpen] = useState(false);
const [wasAboutOpen, setWasAboutOpen] = useState(false);
const [changeLogOpen, setChangeLogOpen] = useState(false);
function SetSidebar(props: { path: string | null }) {
useEffect(() => {
@@ -62,6 +67,8 @@ export default function SiteLayout() {
"control.controller_url": (new URL(webRoot, document.location.href)).href,
});
saveJson("config-control.json", config.toJSON());
} else if (key === "about") {
setAboutOpen(true);
} else if (key === "logOut") {
account.logOut();
}
@@ -74,6 +81,7 @@ export default function SiteLayout() {
</Tooltip>,
key: "ctlConfig",
},
{ label: "About", key: "about" },
{ type: "divider" },
{ label: "Log out", danger: true, key: "logOut" },
],
@@ -142,11 +150,42 @@ export default function SiteLayout() {
}
}}
>
<AboutModal
open={aboutOpen}
onClose={() => {
setAboutOpen(false);
setWasAboutOpen(false);
}}
onOpenChangelog={() => {
setChangeLogOpen(true);
setWasAboutOpen(aboutOpen);
setAboutOpen(false);
}}
/>
<ChangeLogModal
open={changeLogOpen}
onClose={() => {
setChangeLogOpen(false);
setAboutOpen(wasAboutOpen);
}}
/>
<DraggingContext.Provider value={dragging > 0}>
<Header className="header">
<div className="site-logo" />
<span className="site-name">Clusterio</span>
<ChangeLogModal />
<Tooltip
title="View changelog"
placement="bottom"
align={{ offset: [0, -10] }}
>
<span
className="site-version"
style={{ cursor: "pointer" }}
onClick={() => setChangeLogOpen(true)}
>
{webUiPackage.version}
</span>
</Tooltip>
<Menu
theme="dark"
mode="horizontal"
+15
View File
@@ -1,9 +1,20 @@
"use strict";
const os = require("os");
const { execSync } = require("child_process");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const TerserPlugin = require("terser-webpack-plugin");
function getCommitHash() {
try {
return execSync("git rev-parse --short HEAD").toString().trim();
} catch {
return "unknown";
}
}
module.exports = (env = {}, argv = {}) => ({
mode: argv.mode ?? "development",
devtool: argv.mode === "production" ? "source-map" : "eval-source-map",
@@ -29,6 +40,10 @@ module.exports = (env = {}, argv = {}) => ({
new webpack.DefinePlugin({
"process.env.APP_ENV": JSON.stringify("browser"),
"process.env.COMMIT_HASH": JSON.stringify(getCommitHash()),
"process.env.BUILD_DATE": JSON.stringify(new Date().toISOString()),
"process.env.BUILD_ENV": JSON.stringify(process.env.NODE_ENV ?? "dev"),
"process.env.BUILD_OS": JSON.stringify(os.platform()),
}),
// Make sure ant-design icons use the ES variant