cdslogviewer/frontend/app/LogSelector.tsx (189 lines of code) (raw):
import React, { useState, useEffect } from "react";
import { TreeItem, TreeItemClassKey, TreeView } from "@material-ui/lab";
import axios from "axios";
import { formatError } from "./common/format_error";
import {
ArrowDropDown,
ChevronRight,
ExpandMore,
KeyboardArrowDown,
Replay,
} from "@material-ui/icons";
import {
CircularProgress,
Grid,
IconButton,
makeStyles,
setRef,
Typography,
} from "@material-ui/core";
import { loadLogsForRoute } from "./data-loading";
import { formatBytes } from "./common/bytesformatter";
import clsx from "clsx";
import { useNavigate, useParams } from "react-router";
import {
SystemNotification,
SystemNotifcationKind,
} from "@guardian/pluto-headers";
interface LogLabelProps {
label: string;
size: number;
className?: string;
}
const LogLabel: React.FC<LogLabelProps> = (props) => {
return (
<Grid container direction="row" justify="space-between">
<Grid item>
<Typography className={props.className}>{props.label}</Typography>
</Grid>
<Grid item>
<Typography className={props.className}>
{formatBytes(props.size)}
</Typography>
</Grid>
</Grid>
);
};
interface RouteEntryProps {
routeName: string;
childKey: any;
refreshGeneration: number;
logWasSelected: (routeName: string, logName: string) => void;
loadingStatusChanged: (loadingStatus: boolean) => void;
}
const RouteEntry: React.FC<RouteEntryProps> = (props) => {
const [childLogs, setChildLogs] = useState<LogInfo[]>([]);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
if (props.refreshGeneration > 0) {
//we are called on initial load, which is handled below
setChildLogs([]);
loadLogsForRoute(props.routeName, (newData) =>
setChildLogs((prevState) => prevState.concat(newData))
).catch((err) => {
SystemNotification.open(
SystemNotifcationKind.Error,
`Could not load logs - ${formatError(err, false)}`
);
console.error(
"Could not load in logs for ",
props.routeName,
": ",
err
);
});
}
}, [props.refreshGeneration]);
const handleToggle = (evt: React.MouseEvent<Element, MouseEvent>) => {
if (!isLoaded) {
props.loadingStatusChanged(true);
setIsLoaded(true);
if (evt) evt.persist();
loadLogsForRoute(props.routeName, (newData) =>
setChildLogs((prevState) => prevState.concat(newData))
)
.then(() => {
if (evt) evt.target.dispatchEvent(evt.nativeEvent);
props.loadingStatusChanged(false);
})
.catch((err) => {
console.error(
`Could not load in logs for route ${props.routeName}`,
err
);
setIsLoaded(false);
SystemNotification.open(
SystemNotifcationKind.Error,
`Could not load logs - ${formatError(err, false)}`
);
});
}
};
return (
<TreeItem
nodeId={props.routeName}
label={props.routeName}
key={props.childKey}
expandIcon={<ChevronRight />}
collapseIcon={<KeyboardArrowDown />}
endIcon={<ChevronRight />}
onIconClick={handleToggle}
onLabelClick={handleToggle}
>
{childLogs.map((info, idx) => (
<TreeItem
nodeId={info.name}
label={<LogLabel label={info.name} size={info.size} />}
key={idx}
onLabelClick={() => props.logWasSelected(props.routeName, info.name)}
/>
))}
</TreeItem>
);
};
interface LogSelectorProps {
className?: string;
onError?: (errorDesc: string) => void;
onNotLoggedIn?: () => void;
rightColumnExtent: number;
}
const useStyles = makeStyles((theme) => ({
progressIndicator: {
color: theme.palette.secondary.light,
width: "20px",
height: "20px",
},
container: {
listStyle: "none",
padding: 0,
overflowY: "scroll",
overflowX: "hidden",
},
spinner: {
animation: "1.5s linear infinite spinner",
},
}));
const LogSelector: React.FC<LogSelectorProps> = (props) => {
const [knownRoutes, setKnownRoutes] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [expanded, setExpanded] = React.useState<string[]>([]);
const [selected, setSelected] = React.useState<string>("");
const [refreshGeneration, setRefreshGeneration] = useState(0);
const classes = useStyles();
const { routename, podname } = useParams<{
routename: string | undefined;
podname: string | undefined;
}>();
const history = useNavigate();
useEffect(() => {
loadKnownRoutes();
}, []);
useEffect(() => {
if (knownRoutes.length > 0) {
if (routename) setExpanded([routename]);
}
}, [routename, knownRoutes]);
useEffect(() => {
if (knownRoutes.length > 0) {
if (podname) setSelected(podname);
}
}, [podname, knownRoutes]);
const loadKnownRoutes = async () => {
try {
const response = await axios.get<string[]>("/api/routes");
setKnownRoutes(response.data.sort());
setIsLoading(false);
} catch (err) {
setIsLoading(false);
console.error("Could not list known routes: ", err);
if (props.onError) {
props.onError(formatError(err, false));
}
if (axios.isAxiosError(err) && err.response && props.onNotLoggedIn) {
if (err.response.status == 403 || err.response.status == 401)
props.onNotLoggedIn();
}
}
};
const logSelectionDidChange = (routeName: string, logName: string) => {
history(`/log/${routeName}/${logName}`);
};
const handleToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => {
setExpanded(nodeIds);
};
const handleSelect = (event: React.ChangeEvent<{}>, nodeIds: string) => {
setSelected(nodeIds);
};
const refreshLogSelector = async () => {
setIsLoading(true);
await loadKnownRoutes();
setRefreshGeneration((prev) => prev + 1);
setIsLoading(false);
};
return (
<ul className={clsx(props.className, classes.container)}>
<li>
<Grid container direction="row" justify="space-between">
<Grid item>
<Typography variant="h4">Jobs</Typography>
</Grid>
<Grid item>
<IconButton onClick={refreshLogSelector}>
<Replay className={isLoading ? classes.spinner : undefined} />
</IconButton>
</Grid>
</Grid>
</li>
<li>
<TreeView
defaultExpandIcon={<ExpandMore />}
defaultCollapseIcon={<ChevronRight />}
expanded={expanded}
selected={selected}
onNodeToggle={handleToggle}
onNodeSelect={handleSelect}
>
{knownRoutes.length == 0 ? (
<Typography variant="caption">No routes loaded</Typography>
) : (
knownRoutes.map((name, idx) => (
<RouteEntry
routeName={name}
refreshGeneration={refreshGeneration}
childKey={idx}
key={idx}
logWasSelected={logSelectionDidChange}
loadingStatusChanged={(newStatus) => setIsLoading(newStatus)}
/>
))
)}
</TreeView>
</li>
</ul>
);
};
export default LogSelector;