UI: Active Scripts changes (followup to #933) (#1083)

This commit is contained in:
Snarling 2024-02-10 03:59:20 -05:00 committed by GitHub
parent 187226a30f
commit 9697a82e0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 166 additions and 201 deletions

@ -239,9 +239,12 @@ export function refreshTheme(): void {
}, },
MuiIconButton: { MuiIconButton: {
styleOverrides: { styleOverrides: {
root: { root: ({ ownerState }) => ({
color: Settings.theme.primary, color: Settings.theme.primary,
}, ...(ownerState.disableRipple && {
p: 0,
}),
}),
}, },
}, },
MuiTooltip: { MuiTooltip: {

@ -1,21 +1,68 @@
/** import type { WorkerScript } from "../../Netscript/WorkerScript";
* Root React Component for the "Active Scripts" UI page. This page displays import React, { useState } from "react";
* and provides information about all of the player's scripts that are currently running
*/ import { MenuItem, Typography, Select, SelectChangeEvent, TextField, IconButton, List } from "@mui/material";
import React from "react"; import { FirstPage, KeyboardArrowLeft, KeyboardArrowRight, LastPage, Search } from "@mui/icons-material";
import { ScriptProduction } from "./ScriptProduction"; import { ScriptProduction } from "./ScriptProduction";
import { ServerAccordions } from "./ServerAccordions"; import { ServerAccordion } from "./ServerAccordion";
import { WorkerScript } from "../../Netscript/WorkerScript"; import { workerScripts } from "../../Netscript/WorkerScripts";
import { getRecordEntries } from "../../Types/Record";
import { Settings } from "../../Settings/Settings";
import { isPositiveInteger } from "../../types";
import Typography from "@mui/material/Typography"; export function ActiveScriptsPage(): React.ReactElement {
const [scriptsPerPage, setScriptsPerPage] = useState(Settings.ActiveScriptsScriptPageSize);
const [serversPerPage, setServersPerPage] = useState(Settings.ActiveScriptsServerPageSize);
const [filter, setFilter] = useState("");
const [page, setPage] = useState(0);
interface IProps { function changeScriptsPerPage(e: SelectChangeEvent<number>) {
workerScripts: Map<number, WorkerScript>; const n = parseInt(e.target.value as string);
} if (!isPositiveInteger(n)) return;
Settings.ActiveScriptsScriptPageSize = n;
setScriptsPerPage(n);
}
function changeServersPerPage(e: SelectChangeEvent<number>) {
const n = parseInt(e.target.value as string);
if (!isPositiveInteger(n)) return;
Settings.ActiveScriptsServerPageSize = n;
setServersPerPage(n);
}
const serverData: [string, WorkerScript[]][] = (() => {
const tempData: Record<string, WorkerScript[]> = {};
if (filter) {
// Only check filtering if a filter exists (performance)
for (const ws of workerScripts.values()) {
if (!ws.hostname.includes(filter) && !ws.scriptRef.filename.includes(filter)) continue;
const hostname = ws.hostname;
if (tempData[hostname]) tempData[hostname].push(ws);
else tempData[hostname] = [ws];
}
} else {
for (const ws of workerScripts.values()) {
const hostname = ws.hostname;
if (tempData[hostname]) tempData[hostname].push(ws);
else tempData[hostname] = [ws];
}
}
return getRecordEntries(tempData);
})();
const lastPage = Math.max(Math.ceil(serverData.length / serversPerPage) - 1, 0);
function changePage(n: number) {
if (!Number.isInteger(n) || n > lastPage || n < 0) return;
setPage(n);
}
if (page > lastPage) changePage(lastPage);
const adjustedIndex = page * serversPerPage;
const dataToShow = serverData.slice(adjustedIndex, adjustedIndex + serversPerPage);
const firstServerNumber = serverData.length === 0 ? 0 : adjustedIndex + 1;
const lastServerNumber = serverData.length === 0 ? 0 : adjustedIndex + dataToShow.length;
export function ActiveScriptsPage(props: IProps): React.ReactElement {
return ( return (
<> <>
<Typography> <Typography>
@ -25,7 +72,50 @@ export function ActiveScriptsPage(props: IProps): React.ReactElement {
</Typography> </Typography>
<ScriptProduction /> <ScriptProduction />
<ServerAccordions {...props} /> <div style={{ width: "100%", display: "flex", alignItems: "center" }}>
<TextField
value={filter}
onChange={(e) => setFilter(e.target.value)}
autoFocus
InputProps={{ startAdornment: <Search />, spellCheck: false }}
size="small"
/>
<Typography marginLeft="1em">Servers/page:</Typography>
<Select value={serversPerPage} onChange={changeServersPerPage}>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={15}>15</MenuItem>
<MenuItem value={20}>20</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
<Typography marginLeft="1em">Scripts/page:</Typography>
<Select value={scriptsPerPage} onChange={changeScriptsPerPage}>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={15}>15</MenuItem>
<MenuItem value={20}>20</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
<Typography
marginLeft="auto"
marginRight="1em"
>{`${firstServerNumber}-${lastServerNumber} of ${serverData.length}`}</Typography>
<IconButton onClick={() => changePage(0)} disabled={page === 0}>
<FirstPage />
</IconButton>
<IconButton onClick={() => changePage(page - 1)} disabled={page === 0}>
<KeyboardArrowLeft />
</IconButton>
<IconButton onClick={() => changePage(page + 1)} disabled={page === lastPage}>
<KeyboardArrowRight />
</IconButton>
<IconButton onClick={() => changePage(lastPage)} disabled={page === lastPage}>
<LastPage />
</IconButton>
</div>
<List dense={true}>
{dataToShow.map(([hostname, scripts]) => (
<ServerAccordion key={hostname} hostname={hostname} scripts={scripts} />
))}
</List>
</> </>
); );
} }

@ -8,14 +8,9 @@ import Tab from "@mui/material/Tab";
import { ActiveScriptsPage } from "./ActiveScriptsPage"; import { ActiveScriptsPage } from "./ActiveScriptsPage";
import { RecentScriptsPage } from "./RecentScriptsPage"; import { RecentScriptsPage } from "./RecentScriptsPage";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { useRerender } from "../React/hooks"; import { useRerender } from "../React/hooks";
interface IProps { export function ActiveScriptsRoot(): React.ReactElement {
workerScripts: Map<number, WorkerScript>;
}
export function ActiveScriptsRoot(props: IProps): React.ReactElement {
const [tab, setTab] = useState<"active" | "recent">("active"); const [tab, setTab] = useState<"active" | "recent">("active");
useRerender(400); useRerender(400);
@ -29,7 +24,7 @@ export function ActiveScriptsRoot(props: IProps): React.ReactElement {
<Tab label={"Recently Killed"} value={"recent"} /> <Tab label={"Recently Killed"} value={"recent"} />
</Tabs> </Tabs>
{tab === "active" && <ActiveScriptsPage workerScripts={props.workerScripts} />} {tab === "active" && <ActiveScriptsPage />}
{tab === "recent" && <RecentScriptsPage />} {tab === "recent" && <RecentScriptsPage />}
</> </>
); );

@ -16,26 +16,30 @@ import ExpandMore from "@mui/icons-material/ExpandMore";
import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandLess from "@mui/icons-material/ExpandLess";
import { ServerAccordionContent } from "./ServerAccordionContent"; import { ServerAccordionContent } from "./ServerAccordionContent";
import { BaseServer } from "../../Server/BaseServer";
import { WorkerScript } from "../../Netscript/WorkerScript"; import { WorkerScript } from "../../Netscript/WorkerScript";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { GetServer } from "../../Server/AllServers";
interface IProps { interface ServerAccordionProps {
server: BaseServer; hostname: string;
workerScripts: WorkerScript[]; scripts: WorkerScript[];
} }
export function ServerAccordion(props: IProps): React.ReactElement { export function ServerAccordion({ hostname, scripts }: ServerAccordionProps): React.ReactElement {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const server = props.server; const server = GetServer(hostname);
if (!server) {
console.error(`Invalid server ${hostname} while displaying active scripts`);
return <></>;
}
// Accordion's header text // Accordion's header text
// TODO: calculate the longest hostname length rather than hard coding it // TODO: calculate the longest hostname length rather than hard coding it
const longestHostnameLength = 18; const longestHostnameLength = 18;
const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice( const paddedName = `${hostname}${" ".repeat(longestHostnameLength)}`.slice(
0, 0,
Math.max(server.hostname.length, longestHostnameLength), Math.max(hostname.length, longestHostnameLength),
); );
const barOptions = { const barOptions = {
progress: server.ramUsed / server.maxRam, progress: server.ramUsed / server.maxRam,
@ -44,16 +48,16 @@ export function ServerAccordion(props: IProps): React.ReactElement {
const headerTxt = `${paddedName} ${createProgressBarText(barOptions)}`; const headerTxt = `${paddedName} ${createProgressBarText(barOptions)}`;
return ( return (
<Box component={Paper}> <Paper>
<ListItemButton onClick={() => setOpen((old) => !old)}> <ListItemButton onClick={() => setOpen((old) => !old)}>
<ListItemText primary={<Typography style={{ whiteSpace: "pre-wrap" }}>{headerTxt}</Typography>} /> <ListItemText primary={<Typography style={{ whiteSpace: "pre-wrap" }}>{headerTxt}</Typography>} />
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />} {open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
</ListItemButton> </ListItemButton>
<Box mx={2}> <Box mx={2}>
<Collapse in={open} timeout={0} unmountOnExit> <Collapse in={open} timeout={0} unmountOnExit>
<ServerAccordionContent workerScripts={props.workerScripts} /> <ServerAccordionContent scripts={scripts} />
</Collapse> </Collapse>
</Box> </Box>
</Box> </Paper>
); );
} }

@ -1,47 +1,56 @@
// TODO: Probably roll this into the ServerAccordion component, no real need for a separate component
import React, { useState } from "react"; import React, { useState } from "react";
import { WorkerScript } from "../../Netscript/WorkerScript"; import { WorkerScript } from "../../Netscript/WorkerScript";
import { WorkerScriptAccordion } from "./WorkerScriptAccordion"; import { WorkerScriptAccordion } from "./WorkerScriptAccordion";
import List from "@mui/material/List"; import { IconButton, List, Typography } from "@mui/material";
import TablePagination from "@mui/material/TablePagination";
import { TablePaginationActionsAll } from "../React/TablePaginationActionsAll";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { FirstPage, KeyboardArrowLeft, KeyboardArrowRight, LastPage } from "@mui/icons-material";
interface IProps { interface ServerActiveScriptsProps {
workerScripts: WorkerScript[]; scripts: WorkerScript[];
} }
export function ServerAccordionContent(props: IProps): React.ReactElement { export function ServerAccordionContent({ scripts }: ServerActiveScriptsProps): React.ReactElement {
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(Settings.ActiveScriptsScriptPageSize); if (scripts.length === 0) {
const handleChangePage = (event: unknown, newPage: number): void => { console.error(`Attempted to display a server in active scripts when there were no matching scripts to show`);
setPage(newPage); return <></>;
}; }
const scriptsPerPage = Settings.ActiveScriptsScriptPageSize;
const lastPage = Math.ceil(scripts.length / scriptsPerPage) - 1;
function changePage(n: number) {
if (!Number.isInteger(n) || n > lastPage || n < 0) return;
setPage(n);
}
if (page > lastPage) changePage(lastPage);
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>): void => { const firstScriptNumber = page * scriptsPerPage + 1;
Settings.ActiveScriptsScriptPageSize = parseInt(event.target.value, 10); const lastScriptNumber = Math.min((page + 1) * scriptsPerPage, scripts.length);
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
return ( return (
<> <>
{props.workerScripts.length > 10 ? ( <div style={{ width: "100%", display: "flex", alignItems: "center" }}>
<TablePagination <Typography
rowsPerPageOptions={[10, 15, 20, 100]} component="span"
component="div" marginRight="auto"
count={props.workerScripts.length} >{`Displaying scripts ${firstScriptNumber}-${lastScriptNumber} of ${scripts.length}`}</Typography>
rowsPerPage={rowsPerPage} <IconButton onClick={() => changePage(0)} disabled={page === 0}>
page={page} <FirstPage />
onPageChange={handleChangePage} </IconButton>
onRowsPerPageChange={handleChangeRowsPerPage} <IconButton onClick={() => changePage(page - 1)} disabled={page === 0}>
ActionsComponent={TablePaginationActionsAll} <KeyboardArrowLeft />
/> </IconButton>
) : ( <IconButton onClick={() => changePage(page + 1)} disabled={page === lastPage}>
"" <KeyboardArrowRight />
)} </IconButton>
<IconButton onClick={() => changePage(lastPage)} disabled={page === lastPage}>
<LastPage />
</IconButton>
</div>
<List dense disablePadding> <List dense disablePadding>
{props.workerScripts.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((ws) => ( {scripts.slice(page * scriptsPerPage, page * scriptsPerPage + scriptsPerPage).map((ws) => (
<WorkerScriptAccordion key={`${ws.pid}`} workerScript={ws} /> <WorkerScriptAccordion key={ws.pid} workerScript={ws} />
))} ))}
</List> </List>
</> </>

@ -1,135 +0,0 @@
/**
* React Component for rendering the Accordion elements for all servers
* on which scripts are running
*/
import React, { useState } from "react";
import { ServerAccordion } from "./ServerAccordion";
import TextField from "@mui/material/TextField";
import List from "@mui/material/List";
import TablePagination from "@mui/material/TablePagination";
import Grid from "@mui/material/Grid";
import { WorkerScript } from "../../Netscript/WorkerScript";
import { GetServer } from "../../Server/AllServers";
import { BaseServer } from "../../Server/BaseServer";
import { Settings } from "../../Settings/Settings";
import { TablePaginationActionsAll } from "../React/TablePaginationActionsAll";
import SearchIcon from "@mui/icons-material/Search";
import { matchScriptPathUnanchored } from "../../utils/helpers/scriptKey";
import lodash from "lodash";
// Map of server hostname -> all workerscripts on that server for all active scripts
interface IServerData {
server: BaseServer;
workerScripts: WorkerScript[];
}
type IServerToScriptsMap = Record<string, IServerData | undefined>;
interface IProps {
workerScripts: Map<number, WorkerScript>;
}
export function ServerAccordions(props: IProps): React.ReactElement {
const [filter, setFilter] = useState("");
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(Settings.ActiveScriptsServerPageSize);
const handleChangePage = (event: unknown, newPage: number): void => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>): void => {
Settings.ActiveScriptsServerPageSize = parseInt(event.target.value, 10);
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value);
setPage(0);
}
const serverToScriptMap: IServerToScriptsMap = {};
for (const ws of props.workerScripts.values()) {
const server = GetServer(ws.hostname);
if (server == null) {
console.warn(`WorkerScript has invalid hostname: ${ws.hostname}`);
continue;
}
let data = serverToScriptMap[server.hostname];
if (data === undefined) {
serverToScriptMap[server.hostname] = {
server: server,
workerScripts: [],
};
data = serverToScriptMap[server.hostname];
}
if (data !== undefined) {
// Add only scripts that correspond to the filter
if (ws.hostname.includes(filter) || ws.name.includes(filter)) {
data.workerScripts.push(ws);
}
}
}
// Match filter in the scriptname part of the key
const pattern = matchScriptPathUnanchored(lodash.escapeRegExp(filter));
const filtered = Object.values(serverToScriptMap).filter((data) => {
if (!data) return false;
if (data.server.hostname.includes(filter)) return true;
for (const k of data.server.runningScriptMap.keys()) {
if (pattern.test(k)) return true;
}
return false;
});
return (
<>
<Grid container>
<Grid item xs={4}>
<TextField
value={filter}
onChange={handleFilterChange}
autoFocus
InputProps={{
startAdornment: <SearchIcon />,
spellCheck: false,
}}
style={{
paddingTop: "8px",
}}
/>
</Grid>
<Grid item xs={8}>
{filtered.length > 10 ? (
<TablePagination
rowsPerPageOptions={[10, 15, 20, 100]}
component="div"
count={filtered.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActionsAll}
/>
) : (
""
)}
</Grid>
</Grid>
<List dense={true}>
{filtered.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((data) => {
return (
data && (
<ServerAccordion key={data.server.hostname} server={data.server} workerScripts={data.workerScripts} />
)
);
})}
</List>
</>
);
}

@ -51,7 +51,6 @@ import { StaneksGiftRoot } from "../CotMG/ui/StaneksGiftRoot";
import { staneksGift } from "../CotMG/Helper"; import { staneksGift } from "../CotMG/Helper";
import { CharacterOverview } from "./React/CharacterOverview"; import { CharacterOverview } from "./React/CharacterOverview";
import { BladeburnerCinematic } from "../Bladeburner/ui/BladeburnerCinematic"; import { BladeburnerCinematic } from "../Bladeburner/ui/BladeburnerCinematic";
import { workerScripts } from "../Netscript/WorkerScripts";
import { Unclickable } from "../Exploits/Unclickable"; import { Unclickable } from "../Exploits/Unclickable";
import { Snackbar, SnackbarProvider } from "./React/Snackbar"; import { Snackbar, SnackbarProvider } from "./React/Snackbar";
import { LogBoxManager } from "./React/LogBoxManager"; import { LogBoxManager } from "./React/LogBoxManager";
@ -254,7 +253,7 @@ export function GameRoot(): React.ReactElement {
break; break;
} }
case Page.ActiveScripts: { case Page.ActiveScripts: {
mainPage = <ActiveScriptsRoot workerScripts={workerScripts} />; mainPage = <ActiveScriptsRoot />;
break; break;
} }
case Page.Hacknet: { case Page.Hacknet: {