Skip to content

Commit 788c7bc

Browse files
authored
feat: new em ui enclave logs (#1696)
## Description: This PR implements the views for showing the logs in the new enclave manager ui. This change adds react-virtuoso for buffering the logs in a new `LogViewer.tsx` component. `lodash` is used to `throttle` the changes - I believe this solves the issue reported previously with log tailing failing. Once the service log ui is implemented I'll validate this with the log spamming package. A utility `FeatureNotImplemented` modal is added to display more tidy incomplete ends of exploration (per the mocks). ### Demo https://github.com/kurtosis-tech/kurtosis/assets/4419574/18782856-5f0a-4e31-835d-6d840bbfda5c ## Is this change user facing? NO - this is not currently deployed
1 parent 6d43efd commit 788c7bc

File tree

20 files changed

+538
-51
lines changed

20 files changed

+538
-51
lines changed

enclave-manager/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@tanstack/react-table": "^8.10.7",
1313
"enclave-manager-sdk": "file:../api/typescript",
1414
"framer-motion": "^10.16.4",
15+
"lodash": "^4.17.21",
1516
"luxon": "^3.4.3",
1617
"react": "^18.2.0",
1718
"react-dom": "^18.2.0",
@@ -20,6 +21,7 @@
2021
"react-markdown": "^9.0.0",
2122
"react-router-dom": "^6.17.0",
2223
"react-scripts": "5.0.1",
24+
"react-virtuoso": "^4.6.2",
2325
"true-myth": "^7.1.0"
2426
},
2527
"devDependencies": {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Button, ButtonProps } from "@chakra-ui/react";
2+
import { FiDownload } from "react-icons/fi";
3+
import { isDefined } from "../utils";
4+
import { saveTextAsFile } from "../utils/download";
5+
6+
type DownloadButtonProps = ButtonProps & {
7+
valueToDownload?: (() => string) | string | null;
8+
fileName: string;
9+
text?: string;
10+
};
11+
12+
export const DownloadButton = ({ valueToDownload, text, fileName, ...buttonProps }: DownloadButtonProps) => {
13+
const handleDownloadClick = () => {
14+
if (isDefined(valueToDownload)) {
15+
const v = typeof valueToDownload === "string" ? valueToDownload : valueToDownload();
16+
saveTextAsFile(v, fileName);
17+
}
18+
};
19+
20+
if (!isDefined(valueToDownload)) {
21+
return null;
22+
}
23+
24+
return (
25+
<Button
26+
leftIcon={<FiDownload />}
27+
size={"xs"}
28+
colorScheme={"darkBlue"}
29+
onClick={handleDownloadClick}
30+
{...buttonProps}
31+
>
32+
{text || "Download"}
33+
</Button>
34+
);
35+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { KurtosisAlertModal } from "./KurtosisAlertModal";
2+
3+
type FeatureNotImplementedModalProps = {
4+
featureName: string;
5+
message?: string;
6+
isOpen: boolean;
7+
onClose: () => void;
8+
};
9+
10+
export const FeatureNotImplementedModal = ({
11+
featureName,
12+
message,
13+
isOpen,
14+
onClose,
15+
}: FeatureNotImplementedModalProps) => {
16+
return (
17+
<KurtosisAlertModal
18+
title={`${featureName} unavailable`}
19+
isOpen={isOpen}
20+
onClose={onClose}
21+
confirmText={"Submit Request"}
22+
onConfirm={() => {
23+
onClose();
24+
window.open("https://github.com/kurtosis-tech/kurtosis/issues", "_blank");
25+
}}
26+
confirmButtonProps={{ colorScheme: "kurtosisGreen" }}
27+
content={
28+
message || `${featureName} is not currently available. Please open a feature request if you'd like to use this.`
29+
}
30+
/>
31+
);
32+
};

enclave-manager/web/src/components/KurtosisAlertModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type KurtosisAlertModalProps = {
1616
title: string;
1717
content: string;
1818
isOpen: boolean;
19-
isLoading: boolean;
19+
isLoading?: boolean;
2020
onClose: () => void;
2121
onConfirm: () => void;
2222
confirmText: string;
@@ -45,7 +45,7 @@ export const KurtosisAlertModal = ({
4545
<ModalFooter>
4646
<Flex justifyContent={"flex-end"} gap={"12px"}>
4747
<Button color={"gray.100"} onClick={onClose} disabled={isLoading}>
48-
Cancel
48+
Dismiss
4949
</Button>
5050
<Button onClick={onConfirm} {...confirmButtonProps} isLoading={isLoading}>
5151
{confirmText}

enclave-manager/web/src/components/KurtosisThemeProvider.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ const theme = extendTheme({
204204
},
205205
},
206206
Switch: {
207+
defaultProps: {
208+
colorScheme: "green",
209+
},
207210
baseStyle: defineStyle((props) => ({
208211
track: {
209212
_checked: {

enclave-manager/web/src/components/enclaves/configuration/inputs/BooleanArgumentInput.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export const BooleanArgumentInput = ({ inputType, ...props }: BooleanArgumentInp
1212
if (inputType === "switch") {
1313
return (
1414
<Switch
15-
colorScheme={"green"}
1615
{...register(props.name, {
1716
disabled: props.disabled,
1817
required: props.isRequired,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Box } from "@chakra-ui/react";
2+
3+
export type LogStatus = "info" | "error";
4+
5+
export type LogLineProps = {
6+
message?: string;
7+
status?: LogStatus;
8+
};
9+
10+
export const LogLine = ({ message, status }: LogLineProps) => {
11+
const statusToColor = (status?: LogStatus) => {
12+
switch (status) {
13+
case "error":
14+
return "red.400";
15+
case "info":
16+
return "gray.100";
17+
default:
18+
return "white";
19+
}
20+
};
21+
22+
return (
23+
<Box
24+
as={"pre"}
25+
whiteSpace={"pre-wrap"}
26+
borderBottom={"1px solid #444444"}
27+
p={"14px 0"}
28+
m={"0 16px"}
29+
fontSize={"xs"}
30+
lineHeight="2"
31+
fontWeight={400}
32+
fontFamily="Ubuntu Mono"
33+
color={statusToColor(status)}
34+
>
35+
{message || <i>No message</i>}
36+
</Box>
37+
);
38+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Box, ButtonGroup, Flex, FormControl, FormLabel, Progress, Switch } from "@chakra-ui/react";
2+
import { throttle } from "lodash";
3+
import { ChangeEvent, ReactElement, useEffect, useMemo, useRef, useState } from "react";
4+
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
5+
import { isDefined, stripAnsi } from "../../../utils";
6+
import { CopyButton } from "../../CopyButton";
7+
import { DownloadButton } from "../../DownloadButton";
8+
import { LogLine, LogLineProps } from "./LogLine";
9+
10+
type LogViewerProps = {
11+
logLines: LogLineProps[];
12+
progressPercent?: number | "indeterminate" | "failed";
13+
ProgressWidget?: ReactElement;
14+
logsFileName?: string;
15+
};
16+
17+
export const LogViewer = ({
18+
progressPercent,
19+
logLines: propsLogLines,
20+
ProgressWidget,
21+
logsFileName,
22+
}: LogViewerProps) => {
23+
const virtuosoRef = useRef<VirtuosoHandle>(null);
24+
const [logLines, setLogLines] = useState(propsLogLines);
25+
const [automaticScroll, setAutomaticScroll] = useState(true);
26+
27+
const throttledSetLogLines = useMemo(() => throttle(setLogLines, 500), []);
28+
29+
useEffect(() => {
30+
throttledSetLogLines(propsLogLines);
31+
}, [propsLogLines, throttledSetLogLines]);
32+
33+
const handleAutomaticScrollChange = (e: ChangeEvent<HTMLInputElement>) => {
34+
setAutomaticScroll(e.target.checked);
35+
if (virtuosoRef.current && e.target.checked) {
36+
virtuosoRef.current.scrollToIndex({ index: "LAST" });
37+
}
38+
};
39+
40+
const getLogsValue = () => {
41+
return logLines
42+
.map(({ message }) => message)
43+
.filter(isDefined)
44+
.map(stripAnsi)
45+
.join("\n");
46+
};
47+
48+
return (
49+
<Flex flexDirection={"column"} gap={"32px"}>
50+
<Flex flexDirection={"column"} position={"relative"} bg={"gray.800"} borderRadius={"8px"}>
51+
{isDefined(ProgressWidget) && (
52+
<Box
53+
display={"inline-flex"}
54+
alignItems={"center"}
55+
justifyContent={"center"}
56+
gap={"8px"}
57+
position={"absolute"}
58+
top={"16px"}
59+
right={"16px"}
60+
padding={"24px"}
61+
h={"48px"}
62+
bg={"gray.650"}
63+
borderRadius={"8px"}
64+
fontSize={"xl"}
65+
fontWeight={"semibold"}
66+
zIndex={1}
67+
>
68+
{ProgressWidget}
69+
</Box>
70+
)}
71+
<Virtuoso
72+
ref={virtuosoRef}
73+
followOutput={automaticScroll}
74+
atBottomStateChange={(atBottom) => setAutomaticScroll(atBottom)}
75+
style={{ height: "660px" }}
76+
data={logLines.filter(({ message }) => isDefined(message))}
77+
itemContent={(index, line) => <LogLine {...line} />}
78+
/>
79+
{isDefined(progressPercent) && (
80+
<Progress
81+
value={typeof progressPercent === "number" ? progressPercent : progressPercent === "failed" ? 100 : 0}
82+
isIndeterminate={progressPercent === "indeterminate"}
83+
height={"4px"}
84+
colorScheme={progressPercent === "failed" ? "red.500" : "kurtosisGreen"}
85+
/>
86+
)}
87+
</Flex>
88+
<Flex alignItems={"space-between"} width={"100%"}>
89+
<FormControl display={"flex"} alignItems={"center"}>
90+
<Switch isChecked={automaticScroll} onChange={handleAutomaticScrollChange} />
91+
<FormLabel mb={"0"} marginInlineStart={3}>
92+
Automatic Scroll
93+
</FormLabel>
94+
</FormControl>
95+
<ButtonGroup isAttached>
96+
<CopyButton valueToCopy={getLogsValue} size={"md"} isDisabled={logLines.length === 0} />
97+
<DownloadButton
98+
valueToDownload={getLogsValue}
99+
size={"md"}
100+
fileName={logsFileName || `logs.txt`}
101+
isDisabled={logLines.length === 0}
102+
/>
103+
</ButtonGroup>
104+
</Flex>
105+
</Flex>
106+
);
107+
};

enclave-manager/web/src/components/enclaves/modals/ConfigureEnclaveModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const ConfigureEnclaveModal = ({
8686
assertDefined(innerType2, `Cannot parse a dict argument type without knowing innterType2`);
8787
return isDefined(value)
8888
? Object.entries(value).map(([k, v]) => ({ key: k, value: convertArgValue(innerType2, v) }), {})
89-
: {};
89+
: [];
9090
default:
9191
return value;
9292
}
@@ -165,7 +165,7 @@ export const ConfigureEnclaveModal = ({
165165
{ config: formData, packageId: kurtosisPackage.name, apicInfo: apicInfo.toJson() },
166166
{
167167
method: "post",
168-
action: `/enclave/${enclaveUUID}`,
168+
action: `/enclave/${enclaveUUID}/logs`,
169169
encType: "application/json",
170170
},
171171
);

enclave-manager/web/src/components/theme/tabsTheme.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const tabsTheme = defineMultiStyleConfig({
2222
color: `${props.colorScheme}.400`,
2323
bg: `gray.800`,
2424
},
25+
textTransform: "capitalize",
2526
},
2627
tabpanel: {
2728
padding: "32px 0px",

0 commit comments

Comments
 (0)