Skip to content

Commit

Permalink
feat: emui enhancements (#1847)
Browse files Browse the repository at this point in the history
## Description:
This PR includes a number or emui enhancements:
* Artifacts view
* fix to dict/list input sizes
* Update logging ui to match designs
* Update general app layout to match designs (headers)

Additionally the feedback left on
#1830 is implemented.

### Demo of the artifacts view


https://github.com/kurtosis-tech/kurtosis/assets/4419574/0849e46e-c410-4a07-a65f-bcc74297c53b

## Is this change user facing?
YES
  • Loading branch information
Dartoxian authored Nov 27, 2023
1 parent 82d0058 commit 633ba42
Show file tree
Hide file tree
Showing 49 changed files with 1,752 additions and 803 deletions.
8 changes: 5 additions & 3 deletions enclave-manager/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"ansi-to-html": "^0.7.2",
"enclave-manager-sdk": "file:../api/typescript",
"framer-motion": "^10.16.4",
"has-ansi": "^5.0.1",
"html-react-parser": "^4.2.2",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
Expand All @@ -38,6 +37,7 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/streamsaver": "^2.0.4",
"dotenv-cli": "^6.0.0",
"monaco-editor": "^0.44.0",
"prettier": "3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
Expand All @@ -50,9 +50,11 @@
"prebuild": "rm -rf ../../engine/server/webapp",
"clean": "rm -rf build",
"cleanInstall": "rm -rf node_modules; yarn install",
"start": "REACT_APP_VERSION=$(git fetch origin --tags -q && git describe --dirty --match '[0-9]*' --tags)-development react-scripts start",
"start:prod": "serve -s build",
"start": "REACT_APP_VERSION=$(git fetch origin --tags -q && git describe --dirty --match '[0-9]*' --tags)-development PORT=4000 react-scripts start",
"start:cloud": "REACT_APP_VERSION=$(git fetch origin --tags -q && git describe --dirty --match '[0-9]*' --tags)-cloudDevelopment BROWSER=none PUBLIC_URL=http://localhost:3000/emui-dev PORT=4000 dotenv -e ./.env.cloudDevelopment -- react-scripts start",
"start:prod": "serve -p 4000 -s build",
"build": "REACT_APP_VERSION=$(git fetch origin --tags -q && git describe --dirty --match '[0-9]*' --tags) react-scripts build",
"build:cloudDev": "dotenv -e ./.env.cloudDevelopment -- react-scripts build",
"postbuild": "cp -r build/ ../../engine/server/webapp",
"prettier": "prettier . --check",
"prettier:fix": "prettier . --write",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export abstract class KurtosisClient {
}, `KurtosisClient could not listFilesArtifactNamesAndUuids for ${enclave.name}`);
}

async inspectFilesArtifactContents(enclave: RemoveFunctions<EnclaveInfo>, file: FilesArtifactNameAndUuid) {
async inspectFilesArtifactContents(enclave: RemoveFunctions<EnclaveInfo>, fileUuid: string) {
return await asyncResult(() => {
const apicInfo = enclave.apiContainerInfo;
assertDefined(
Expand All @@ -161,7 +161,7 @@ export abstract class KurtosisClient {
const request = new InspectFilesArtifactContentsRequest({
apicIpAddress: apicInfo.bridgeIpAddress,
apicPort: apicInfo.grpcPortInsideEnclave,
fileNamesAndUuid: file,
fileNamesAndUuid: { fileUuid },
});
return this.client.inspectFilesArtifactContents(request, this.getHeaderOptions());
}, `KurtosisClient could not inspectFilesArtifactContents for ${enclave.name}`);
Expand Down
109 changes: 95 additions & 14 deletions enclave-manager/web/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,112 @@
import { Flex } from "@chakra-ui/react";
import React, { PropsWithChildren } from "react";
import { PropsWithChildren, useRef } from "react";
import { Navbar } from "../emui/Navbar";
import { KurtosisBreadcrumbs } from "./KurtosisBreadcrumbs";
import { MAIN_APP_MAX_WIDTH } from "./theme/constants";
import {
MAIN_APP_BOTTOM_PADDING,
MAIN_APP_LEFT_PADDING,
MAIN_APP_MAX_WIDTH,
MAIN_APP_RIGHT_PADDING,
MAIN_APP_TOP_PADDING,
} from "./theme/constants";

type AppLayoutProps = PropsWithChildren<{
Nav: React.ReactElement;
}>;

export const AppLayout = ({ Nav, children }: AppLayoutProps) => {
export const AppLayout = ({ children }: PropsWithChildren) => {
return (
<>
{Nav}
<Navbar />
<Flex
as="main"
w={"100%"}
minH={"calc(100vh - 40px)"}
minH={"100vh"}
justifyContent={"flex-start"}
p={"20px 40px 20px 112px"}
flexDirection={"column"}
className={"app-container"}
>
<Flex maxWidth={MAIN_APP_MAX_WIDTH} w={"100%"}>
<Flex direction={"column"} gap={"36px"} width={"100%"}>
<KurtosisBreadcrumbs />
{children}
</Flex>
</>
);
};

type AppPageLayoutProps = PropsWithChildren<{
preventPageScroll?: boolean;
}>;

export const AppPageLayout = ({ preventPageScroll, children }: AppPageLayoutProps) => {
const headerRef = useRef<HTMLDivElement>(null);
const numberOfChildren = Array.isArray(children) ? children.length : 1;

if (numberOfChildren === 1) {
return (
<Flex
flexDirection={"column"}
w={"100%"}
h={"100%"}
maxHeight={preventPageScroll ? `100vh` : undefined}
flex={"1"}
>
<Flex
flexDirection={"column"}
flex={"1"}
w={"100%"}
h={"100%"}
maxWidth={MAIN_APP_MAX_WIDTH}
pl={MAIN_APP_LEFT_PADDING}
pr={MAIN_APP_RIGHT_PADDING}
>
<KurtosisBreadcrumbs />
<Flex
w={"100%"}
h={"100%"}
pt={MAIN_APP_TOP_PADDING}
pb={MAIN_APP_BOTTOM_PADDING}
flexDirection={"column"}
flex={"1"}
>
{children}
</Flex>
</Flex>
</Flex>
</>
);
}

// TS cannot infer that children is an array if numberOfChildren === 2
if (numberOfChildren === 2 && Array.isArray(children)) {
return (
<Flex direction="column" width={"100%"} h={"100%"} flex={"1"}>
<Flex ref={headerRef} width={"100%"} bg={"gray.850"}>
<Flex
flexDirection={"column"}
width={"100%"}
pl={MAIN_APP_LEFT_PADDING}
pr={MAIN_APP_RIGHT_PADDING}
maxW={MAIN_APP_MAX_WIDTH}
>
<KurtosisBreadcrumbs />
{children[0]}
</Flex>
</Flex>
<Flex
maxWidth={MAIN_APP_MAX_WIDTH}
pl={MAIN_APP_LEFT_PADDING}
pr={MAIN_APP_RIGHT_PADDING}
pt={MAIN_APP_TOP_PADDING}
pb={MAIN_APP_BOTTOM_PADDING}
w={"100%"}
h={"100%"}
flex={"1"}
flexDirection={"column"}
maxHeight={preventPageScroll ? `calc(100vh - ${headerRef.current?.offsetHeight || 0}px)` : undefined}
>
{children[1]}
</Flex>
</Flex>
);
}

throw new Error(
`AppPageLayout expects to receive exactly one or two children. ` +
`If there are two children, the first child is the header section and the next child is the body. ` +
`Otherwise the only child is the body.`,
);
};
179 changes: 117 additions & 62 deletions enclave-manager/web/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,132 @@
import { Box } from "@chakra-ui/react";
import { Editor, OnChange, OnMount } from "@monaco-editor/react";
import { editor } from "monaco-editor";
import { useState } from "react";
import { isDefined } from "../utils";
import { editor as monacoEditor } from "monaco-editor";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { assertDefined, isDefined } from "../utils";

type CodeEditorProps = {
text: string;
fileName?: string;
onTextChange?: (newText: string) => void;
showLineNumbers?: boolean;
};

export const CodeEditor = ({ text, onTextChange, showLineNumbers }: CodeEditorProps) => {
const isReadOnly = !isDefined(onTextChange);
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor>();
export type CodeEditorImperativeAttributes = {
formatCode: () => Promise<void>;
};

export const CodeEditor = forwardRef<CodeEditorImperativeAttributes, CodeEditorProps>(
({ text, fileName, onTextChange, showLineNumbers }, ref) => {
const isReadOnly = !isDefined(onTextChange);
const [editor, setEditor] = useState<monacoEditor.IStandaloneCodeEditor>();

const resizeEditorBasedOnContent = useCallback(() => {
if (isDefined(editor)) {
// An initial layout call is needed, else getContentHeight is garbage
editor.layout();
const contentHeight = editor.getContentHeight();
editor.layout({ width: editor.getContentWidth(), height: contentHeight });
// Unclear why layout must be called twice, but seems to be necessary
editor.layout();
}
}, [editor]);

const resizeEditorBasedOnContent = () => {
if (isDefined(editor)) {
// An initial layout call is needed, else getContentHeight is garbage
editor.layout();
const contentHeight = editor.getContentHeight();
editor.layout({ width: 500, height: contentHeight });
// Unclear why layout must be called twice, but seems to be necessary
editor.layout();
}
};
const handleMount: OnMount = (editor, monaco) => {
setEditor(editor);
const colors: monacoEditor.IColors = {};
if (isReadOnly) {
colors["editor.background"] = "#111111";
}
monaco.editor.defineTheme("kurtosis-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors,
});
monaco.editor.setTheme("kurtosis-theme");
};

const handleMount: OnMount = (editor, monaco) => {
setEditor(editor);
monaco.editor.defineTheme("kurtosis-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {},
});
monaco.editor.setTheme("kurtosis-theme");
};
const handleChange: OnChange = (value, ev) => {
if (isDefined(value) && onTextChange) {
onTextChange(value);
resizeEditorBasedOnContent();
}
};

const handleChange: OnChange = (value, ev) => {
if (isDefined(value) && onTextChange) {
onTextChange(value);
useImperativeHandle(
ref,
() => ({
formatCode: async () => {
console.log("formatting");
if (!isDefined(editor)) {
// do nothing
console.log("no editor");
return;
}
return new Promise((resolve) => {
const listenerDisposer = editor.onDidChangeConfiguration((event) => {
console.log("listener called", event);
if (event.hasChanged(89 /* ID of the readonly option */)) {
console.log("running format");
const formatAction = editor.getAction("editor.action.formatDocument");
assertDefined(formatAction, `Format action is not defined`);
formatAction.run().then(() => {
listenerDisposer.dispose();
editor.updateOptions({
readOnly: isReadOnly,
});
resizeEditorBasedOnContent();
resolve();
});
}
});
console.log("disablin read only");
editor.updateOptions({
readOnly: false,
});
});
},
}),
[isReadOnly, editor, resizeEditorBasedOnContent],
);

useEffect(() => {
// Triggered as the text can change without internal editing. (ie if the
// controlled prop changes)
resizeEditorBasedOnContent();
}
};
}, [text, resizeEditorBasedOnContent]);

// Triggering this on every render seems to keep the editor correctly sized
// it is unclear why this is the case.
resizeEditorBasedOnContent();
// Triggering this on every render seems to keep the editor correctly sized
// it is unclear why this is the case.
resizeEditorBasedOnContent();

return (
<Box width={"100%"}>
<Editor
onMount={handleMount}
value={text}
onChange={handleChange}
options={{
automaticLayout: false, // if this is `true` a ResizeObserver is installed. This causes issues with us managing the container size outside.
readOnly: isReadOnly,
lineNumbers: showLineNumbers || (!isDefined(showLineNumbers) && !isReadOnly) ? "on" : "off",
minimap: { enabled: false },
wordWrap: "on",
wrappingStrategy: "advanced",
scrollBeyondLastLine: false,
renderLineHighlight: isReadOnly ? "none" : "line",
selectionHighlight: !isReadOnly,
occurrencesHighlight: !isReadOnly,
overviewRulerLanes: isReadOnly ? 0 : 3,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
}}
defaultLanguage={"json"}
theme={"vs-dark"}
/>
</Box>
);
};
return (
<Box width={"100%"}>
<Editor
onMount={handleMount}
value={text}
path={fileName}
onChange={handleChange}
options={{
automaticLayout: false, // if this is `true` a ResizeObserver is installed. This causes issues with us managing the container size outside.
readOnly: isReadOnly,
lineNumbers: showLineNumbers || (!isDefined(showLineNumbers) && !isReadOnly) ? "on" : "off",
minimap: { enabled: false },
wordWrap: "on",
wrappingStrategy: "advanced",
scrollBeyondLastLine: false,
renderLineHighlight: isReadOnly ? "none" : "line",
selectionHighlight: !isReadOnly,
occurrencesHighlight: !isReadOnly,
overviewRulerLanes: isReadOnly ? 0 : 3,
scrollbar: {
alwaysConsumeMouseWheel: false,
},
}}
defaultLanguage={!isDefined(fileName) ? "json" : undefined}
theme={"vs-dark"}
/>
</Box>
);
},
);
Loading

0 comments on commit 633ba42

Please sign in to comment.