mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-26 03:00:56 +02:00
UI: Improve UX of Remote API setting page (#1870)
Co-authored-by: David Walker <d0sboots@gmail.com>
This commit is contained in:
@@ -7,34 +7,38 @@ import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../..
|
|||||||
|
|
||||||
export const RemoteAPIPage = (): React.ReactElement => {
|
export const RemoteAPIPage = (): React.ReactElement => {
|
||||||
const [remoteFileApiHostname, setRemoteFileApiHostname] = useState(Settings.RemoteFileApiAddress);
|
const [remoteFileApiHostname, setRemoteFileApiHostname] = useState(Settings.RemoteFileApiAddress);
|
||||||
const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort);
|
const [hostnameError, setHostnameError] = useState(
|
||||||
|
isValidConnectionHostname(Settings.RemoteFileApiAddress).message ?? "",
|
||||||
|
);
|
||||||
|
const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort.toString());
|
||||||
|
const [portError, setPortError] = useState(isValidConnectionPort(Settings.RemoteFileApiPort).message ?? "");
|
||||||
|
|
||||||
|
const isValidHostname = hostnameError === "";
|
||||||
|
const isValidPort = portError === "";
|
||||||
|
|
||||||
function handleRemoteFileApiHostnameChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function handleRemoteFileApiHostnameChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
let newValue = event.target.value.trim();
|
const newValue = event.target.value.trim();
|
||||||
// Empty string will be automatically changed to "localhost".
|
setRemoteFileApiHostname(newValue);
|
||||||
if (newValue === "") {
|
const result = isValidConnectionHostname(newValue);
|
||||||
newValue = "localhost";
|
if (!result.success) {
|
||||||
}
|
setHostnameError(result.message);
|
||||||
if (!isValidConnectionHostname(newValue)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRemoteFileApiHostname(newValue);
|
|
||||||
Settings.RemoteFileApiAddress = newValue;
|
Settings.RemoteFileApiAddress = newValue;
|
||||||
|
setHostnameError("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemoteFileApiPortChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function handleRemoteFileApiPortChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
let newValue = event.target.value.trim();
|
const newValue = event.target.value.trim();
|
||||||
// Empty string will be automatically changed to "0".
|
setRemoteFileApiPort(newValue);
|
||||||
if (newValue === "") {
|
|
||||||
newValue = "0";
|
|
||||||
}
|
|
||||||
const port = Number.parseInt(newValue);
|
const port = Number.parseInt(newValue);
|
||||||
// Disallow invalid ports but still allow the player to set port to 0. Setting it to 0 means that RFA is disabled.
|
const result = isValidConnectionPort(port);
|
||||||
if (port !== 0 && !isValidConnectionPort(port)) {
|
if (!result.success) {
|
||||||
|
setPortError(result.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRemoteFileApiPort(port);
|
|
||||||
Settings.RemoteFileApiPort = port;
|
Settings.RemoteFileApiPort = port;
|
||||||
|
setPortError("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,11 +60,17 @@ export const RemoteAPIPage = (): React.ReactElement => {
|
|||||||
title={
|
title={
|
||||||
<Typography>
|
<Typography>
|
||||||
This hostname is used to connect to a Remote API, please ensure that it matches with your Remote API
|
This hostname is used to connect to a Remote API, please ensure that it matches with your Remote API
|
||||||
hostname. Default: localhost.
|
hostname.
|
||||||
|
<br />
|
||||||
|
If you use IPv6, you need to wrap it in square brackets. For example: [::1]
|
||||||
|
<br />
|
||||||
|
Default: localhost.
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
|
error={!isValidHostname}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <Typography>Hostname: </Typography>,
|
startAdornment: <Typography>Hostname: </Typography>,
|
||||||
}}
|
}}
|
||||||
@@ -69,32 +79,36 @@ export const RemoteAPIPage = (): React.ReactElement => {
|
|||||||
placeholder="localhost"
|
placeholder="localhost"
|
||||||
size={"medium"}
|
size={"medium"}
|
||||||
/>
|
/>
|
||||||
|
{hostnameError && <Typography color={Settings.theme.error}>{hostnameError}</Typography>}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<br />
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<Typography>
|
<Typography>
|
||||||
This port number is used to connect to a Remote API, please ensure that it matches with your Remote API
|
This port number is used to connect to the Remote API. Please ensure that it matches with your Remote API
|
||||||
server port. Set to 0 to disable the feature.
|
server port.
|
||||||
|
<br />
|
||||||
|
The value must be in the range of [0, 65535]. Set it to 0 to disable the feature.
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
|
error={!isValidPort}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: <Typography color={isValidPort ? "success" : "error"}>Port: </Typography>,
|
||||||
<Typography color={isValidConnectionPort(remoteFileApiPort) ? "success" : "error"}>
|
|
||||||
Port:
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
value={remoteFileApiPort}
|
value={remoteFileApiPort}
|
||||||
onChange={handleRemoteFileApiPortChange}
|
onChange={handleRemoteFileApiPortChange}
|
||||||
placeholder="12525"
|
placeholder="12525"
|
||||||
size={"medium"}
|
size={"medium"}
|
||||||
/>
|
/>
|
||||||
|
{portError && <Typography color={Settings.theme.error}>{portError}</Typography>}
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<br />
|
<Button disabled={!isValidHostname || !isValidPort} onClick={newRemoteFileApiConnection}>
|
||||||
<Button onClick={newRemoteFileApiConnection}>Connect</Button>
|
Connect
|
||||||
|
</Button>
|
||||||
</GameOptionsPage>
|
</GameOptionsPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { RFARequestHandler } from "./MessageHandlers";
|
|||||||
import { SnackbarEvents } from "../ui/React/Snackbar";
|
import { SnackbarEvents } from "../ui/React/Snackbar";
|
||||||
import { ToastVariant } from "@enums";
|
import { ToastVariant } from "@enums";
|
||||||
|
|
||||||
|
function showErrorMessage(address: string, detail: string) {
|
||||||
|
SnackbarEvents.emit(`Error with websocket ${address}, details: ${detail}`, ToastVariant.ERROR, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
export class Remote {
|
export class Remote {
|
||||||
connection?: WebSocket;
|
connection?: WebSocket;
|
||||||
static protocol = "ws";
|
static protocol = "ws";
|
||||||
@@ -20,11 +24,14 @@ export class Remote {
|
|||||||
|
|
||||||
public startConnection(): void {
|
public startConnection(): void {
|
||||||
const address = Remote.protocol + "://" + this.ipaddr + ":" + this.port;
|
const address = Remote.protocol + "://" + this.ipaddr + ":" + this.port;
|
||||||
|
try {
|
||||||
this.connection = new WebSocket(address);
|
this.connection = new WebSocket(address);
|
||||||
|
} catch (error) {
|
||||||
this.connection.addEventListener("error", (e: Event) =>
|
console.error(error);
|
||||||
SnackbarEvents.emit(`Error with websocket ${address}, details: ${JSON.stringify(e)}`, ToastVariant.ERROR, 5000),
|
showErrorMessage(address, String(error));
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
this.connection.addEventListener("error", (e: Event) => showErrorMessage(address, JSON.stringify(e)));
|
||||||
this.connection.addEventListener("message", handleMessageEvent);
|
this.connection.addEventListener("message", handleMessageEvent);
|
||||||
this.connection.addEventListener("open", () =>
|
this.connection.addEventListener("open", () =>
|
||||||
SnackbarEvents.emit(
|
SnackbarEvents.emit(
|
||||||
|
|||||||
+32
-11
@@ -4,6 +4,7 @@ import { defaultStyles } from "../Themes/Styles";
|
|||||||
import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options";
|
import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options";
|
||||||
import { defaultMonacoTheme } from "../ScriptEditor/ui/themes";
|
import { defaultMonacoTheme } from "../ScriptEditor/ui/themes";
|
||||||
import { objectAssert } from "../utils/helpers/typeAssertion";
|
import { objectAssert } from "../utils/helpers/typeAssertion";
|
||||||
|
import { Result } from "../types";
|
||||||
import {
|
import {
|
||||||
assertAndSanitizeEditorTheme,
|
assertAndSanitizeEditorTheme,
|
||||||
assertAndSanitizeMainTheme,
|
assertAndSanitizeMainTheme,
|
||||||
@@ -21,7 +22,14 @@ import {
|
|||||||
* - Use non-http schemes in the hostname: "ftp://a.com"
|
* - Use non-http schemes in the hostname: "ftp://a.com"
|
||||||
* - etc.
|
* - etc.
|
||||||
*/
|
*/
|
||||||
export function isValidConnectionHostname(hostname: string): boolean {
|
export function isValidConnectionHostname(hostname: string): Result {
|
||||||
|
// Return a user-friendly error message.
|
||||||
|
if (hostname === "") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Hostname cannot be empty",
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes:
|
* We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes:
|
||||||
* - Specify a scheme: http or https.
|
* - Specify a scheme: http or https.
|
||||||
@@ -31,23 +39,36 @@ export function isValidConnectionHostname(hostname: string): boolean {
|
|||||||
try {
|
try {
|
||||||
// Check scheme.
|
// Check scheme.
|
||||||
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Do not specify scheme (e.g., http, https)",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// Parse to a URL with a default scheme.
|
// Parse to a URL with a default scheme.
|
||||||
const url = new URL(`http://${hostname}`);
|
const url = new URL(`http://${hostname}`);
|
||||||
// Check port, pathname, and search params.
|
// Check port, pathname, and search params.
|
||||||
if (url.port !== "" || url.pathname !== "/" || url.search !== "") {
|
if (url.port !== "" || url.pathname !== "/" || url.search !== "") {
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Do not specify port, pathname, or search parameters",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error(`Invalid hostname: ${hostname}`, e);
|
console.error(error);
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Invalid hostname: ${hostname}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return true;
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidConnectionPort(port: number): boolean {
|
export function isValidConnectionPort(port: number): Result {
|
||||||
return Number.isFinite(port) && port > 0 && port <= 65535;
|
// 0 is a special value for port. It's an invalid port, but the player can use it to disable RFA.
|
||||||
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
||||||
|
return { success: false, message: "Invalid port" };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The current options the player has customized to their play style. */
|
/** The current options the player has customized to their play style. */
|
||||||
@@ -198,10 +219,10 @@ export const Settings = {
|
|||||||
* The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that
|
* The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that
|
||||||
* case, we set them to the default value.
|
* case, we set them to the default value.
|
||||||
*/
|
*/
|
||||||
if (!isValidConnectionHostname(Settings.RemoteFileApiAddress)) {
|
if (!isValidConnectionHostname(Settings.RemoteFileApiAddress).success) {
|
||||||
Settings.RemoteFileApiAddress = "localhost";
|
Settings.RemoteFileApiAddress = "localhost";
|
||||||
}
|
}
|
||||||
if (!isValidConnectionPort(Settings.RemoteFileApiPort)) {
|
if (!isValidConnectionPort(Settings.RemoteFileApiPort).success) {
|
||||||
Settings.RemoteFileApiPort = 0;
|
Settings.RemoteFileApiPort = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user