Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: emui enhancements #1847

Merged
merged 15 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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