diff --git a/enclave-manager/web/package.json b/enclave-manager/web/package.json index 246ec58ee2..2bfb026502 100644 --- a/enclave-manager/web/package.json +++ b/enclave-manager/web/package.json @@ -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", @@ -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", @@ -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", diff --git a/enclave-manager/web/src/client/enclaveManager/KurtosisClient.ts b/enclave-manager/web/src/client/enclaveManager/KurtosisClient.ts index 20520c1368..06adfec9a1 100644 --- a/enclave-manager/web/src/client/enclaveManager/KurtosisClient.ts +++ b/enclave-manager/web/src/client/enclaveManager/KurtosisClient.ts @@ -151,7 +151,7 @@ export abstract class KurtosisClient { }, `KurtosisClient could not listFilesArtifactNamesAndUuids for ${enclave.name}`); } - async inspectFilesArtifactContents(enclave: RemoveFunctions, file: FilesArtifactNameAndUuid) { + async inspectFilesArtifactContents(enclave: RemoveFunctions, fileUuid: string) { return await asyncResult(() => { const apicInfo = enclave.apiContainerInfo; assertDefined( @@ -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}`); diff --git a/enclave-manager/web/src/components/AppLayout.tsx b/enclave-manager/web/src/components/AppLayout.tsx index b27857045e..142115f090 100644 --- a/enclave-manager/web/src/components/AppLayout.tsx +++ b/enclave-manager/web/src/components/AppLayout.tsx @@ -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} + - - - + {children} + + + ); +}; + +type AppPageLayoutProps = PropsWithChildren<{ + preventPageScroll?: boolean; +}>; + +export const AppPageLayout = ({ preventPageScroll, children }: AppPageLayoutProps) => { + const headerRef = useRef(null); + const numberOfChildren = Array.isArray(children) ? children.length : 1; + + if (numberOfChildren === 1) { + return ( + + + + {children} - + ); + } + + // TS cannot infer that children is an array if numberOfChildren === 2 + if (numberOfChildren === 2 && Array.isArray(children)) { + return ( + + + + + {children[0]} + + + + {children[1]} + + + ); + } + + 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.`, ); }; diff --git a/enclave-manager/web/src/components/CodeEditor.tsx b/enclave-manager/web/src/components/CodeEditor.tsx index 7eeffa0fea..cf8eed1a6b 100644 --- a/enclave-manager/web/src/components/CodeEditor.tsx +++ b/enclave-manager/web/src/components/CodeEditor.tsx @@ -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(); +export type CodeEditorImperativeAttributes = { + formatCode: () => Promise; +}; + +export const CodeEditor = forwardRef( + ({ text, fileName, onTextChange, showLineNumbers }, ref) => { + const isReadOnly = !isDefined(onTextChange); + const [editor, setEditor] = useState(); + + 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 ( - - - - ); -}; + return ( + + + + ); + }, +); diff --git a/enclave-manager/web/src/components/DataTable.tsx b/enclave-manager/web/src/components/DataTable.tsx index c5ef09abbf..3482d43233 100644 --- a/enclave-manager/web/src/components/DataTable.tsx +++ b/enclave-manager/web/src/components/DataTable.tsx @@ -1,4 +1,4 @@ -import { Button, Icon, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; +import { Box, Button, Icon, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react"; import { ColumnDef, flexRender, @@ -66,55 +66,68 @@ export function DataTable({ }); return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const meta = header.column.columnDef.meta; - return ( - - ); - })} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const meta = cell.column.columnDef.meta; - return ( - - ); - })} - - ))} - -
- {header.column.getCanSort() && ( - - )} - {!header.column.getCanSort() && flexRender(header.column.columnDef.header, header.getContext())} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta; + return ( + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta; + return ( + + ); + })} + + ))} + +
+ {header.column.getCanSort() && ( + + )} + {!header.column.getCanSort() && flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
); } diff --git a/enclave-manager/web/src/components/FileDisplay.tsx b/enclave-manager/web/src/components/FileDisplay.tsx index 1faca42b21..dc76b39914 100644 --- a/enclave-manager/web/src/components/FileDisplay.tsx +++ b/enclave-manager/web/src/components/FileDisplay.tsx @@ -1,7 +1,8 @@ -import { ButtonGroup, Card, Flex, Text } from "@chakra-ui/react"; +import { ButtonGroup } from "@chakra-ui/react"; import { CodeEditor } from "./CodeEditor"; import { CopyButton } from "./CopyButton"; import { DownloadButton } from "./DownloadButton"; +import { TitledCard } from "./TitledCard"; type FileDisplayProps = { title: string; @@ -11,19 +12,21 @@ type FileDisplayProps = { export const FileDisplay = ({ value, filename, title }: FileDisplayProps) => { return ( - - - - {title} - - - - + + + - - - - - + } + > + + ); }; diff --git a/enclave-manager/web/src/components/FileSize.tsx b/enclave-manager/web/src/components/FileSize.tsx new file mode 100644 index 0000000000..e5d1e54e16 --- /dev/null +++ b/enclave-manager/web/src/components/FileSize.tsx @@ -0,0 +1,26 @@ +import { Text, TextProps } from "@chakra-ui/react"; +import { isDefined } from "../utils"; + +type FileSizeProps = TextProps & { + fileSize?: bigint; +}; + +const units = ["B", "KB", "MB", "GB", "TB"]; + +export const FileSize = ({ fileSize, ...textProps }: FileSizeProps) => { + if (!isDefined(fileSize)) { + return null; + } + let size = fileSize; + let unitIndex = 0; + while (size > 1024 && unitIndex < units.length - 1) { + size = size / BigInt(1024); + unitIndex += 1; + } + + return ( + + {`${size}${units[unitIndex]}`} + + ); +}; diff --git a/enclave-manager/web/src/components/FileTree.tsx b/enclave-manager/web/src/components/FileTree.tsx new file mode 100644 index 0000000000..4c002904d5 --- /dev/null +++ b/enclave-manager/web/src/components/FileTree.tsx @@ -0,0 +1,123 @@ +import { Button, Flex, Text } from "@chakra-ui/react"; +import React, { useCallback, useMemo, useState } from "react"; +import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "react-icons/ai"; +import { isDefined } from "../utils"; +import { FileSize } from "./FileSize"; + +/** + * This file tree component recursively renders itself to present a file tree. + * To keep this performant the nodes (DirectoryNode and FileNode) must make use of + * useCallback and useMemo. This allows the React.memo around FileTreeMode to function + * and skip rendering unchanged components. + */ + +export type FileTreeNode = { + name: string; + size?: bigint; + childNodes?: FileTreeNode[]; +}; + +type FileTreeProps = { + nodes: FileTreeNode[]; + selectedFilePath?: string[]; + onFileSelected: (selectedFilePath: string[]) => void; + // Internal prop used for padding + _isChildNode?: boolean; +}; + +export const FileTree = ({ nodes, selectedFilePath, onFileSelected, _isChildNode }: FileTreeProps) => { + return ( + + {nodes.map((node, i) => ( + 0 && selectedFilePath[0] === node.name + ? selectedFilePath + : undefined + } + onFileSelected={onFileSelected} + /> + ))} + + ); +}; + +type FileTreeNodeComponentProps = { + node: FileTreeNode; + selectedFilePath?: string[]; + onFileSelected: (selectedFilePath: string[]) => void; +}; + +const FileTreeNodeComponent = React.memo((props: FileTreeNodeComponentProps) => { + if (isDefined(props.node.childNodes)) { + return ; + } else { + return ; + } +}); + +const DirectoryNode = ({ + node, + selectedFilePath, + onFileSelected, +}: FileTreeNodeComponentProps & { node: { childNodes: FileTreeNode[] } }) => { + const [collapsed, setCollapsed] = useState(false); + + const childSelectedFilePath = useMemo( + () => + isDefined(selectedFilePath) && selectedFilePath.length > 0 && selectedFilePath[0] === node.name + ? selectedFilePath.slice(1) + : undefined, + [selectedFilePath, node], + ); + + const handleClick = useCallback(() => { + setCollapsed((collapsed) => !collapsed); + }, []); + + const handleFileSelected = useCallback( + (filePath: string[]) => onFileSelected([node.name, ...filePath]), + [onFileSelected, node], + ); + + return ( + <> + + {!collapsed && ( + + )} + + ); +}; + +const FileNode = ({ node, selectedFilePath, onFileSelected }: FileTreeNodeComponentProps) => { + const isSelected = isDefined(selectedFilePath) && selectedFilePath.length === 1 && selectedFilePath[0] === node.name; + return ( + + ); +}; diff --git a/enclave-manager/web/src/components/HoverLineTabList.tsx b/enclave-manager/web/src/components/HoverLineTabList.tsx new file mode 100644 index 0000000000..703b34472f --- /dev/null +++ b/enclave-manager/web/src/components/HoverLineTabList.tsx @@ -0,0 +1,43 @@ +import { Tab, TabList } from "@chakra-ui/react"; +import { useState } from "react"; +import { isDefined } from "../utils"; + +type HoverLineTabListProps = { + tabs: string[]; + activeTab: string; +}; + +/** + * This component is needed as the hover interaction cannot be controlled with CSS + */ +export const HoverLineTabList = ({ tabs, activeTab }: HoverLineTabListProps) => { + const [hoveredTab, setHoveredTab] = useState(); + + return ( + + {tabs.map((tab) => { + return ( + setHoveredTab(tab)} + onMouseLeave={() => setHoveredTab(undefined)} + > + {tab} + + ); + })} + + ); +}; diff --git a/enclave-manager/web/src/components/KeyboardCommands.tsx b/enclave-manager/web/src/components/KeyboardCommands.tsx new file mode 100644 index 0000000000..7c759c06f3 --- /dev/null +++ b/enclave-manager/web/src/components/KeyboardCommands.tsx @@ -0,0 +1,15 @@ +import { Text, TextProps } from "@chakra-ui/react"; + +export const FindCommand = (props: TextProps) => { + let text = "^F"; + + if (navigator.userAgent.indexOf("Mac") > -1) { + text = "⌘F"; + } + + return ( + + {text} + + ); +}; diff --git a/enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx b/enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx index 755bb67f83..e61651f247 100644 --- a/enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx +++ b/enclave-manager/web/src/components/KurtosisBreadcrumbs.tsx @@ -1,4 +1,3 @@ -import { ChevronRightIcon } from "@chakra-ui/icons"; import { Breadcrumb, BreadcrumbItem, @@ -12,13 +11,20 @@ import { MenuButton, MenuItem, MenuList, + Text, } from "@chakra-ui/react"; -import { ReactElement, useEffect, useState } from "react"; -import { MdFilterList } from "react-icons/md"; +import { ReactElement, useMemo } from "react"; +import { BsCaretDownFill } from "react-icons/bs"; import { Link, Params, UIMatch, useMatches } from "react-router-dom"; import { EmuiAppState, useEmuiAppContext } from "../emui/EmuiAppContext"; import { isDefined } from "../utils"; import { RemoveFunctions } from "../utils/types"; +import { BREADCRUMBS_HEIGHT, MAIN_APP_MAX_WIDTH_WITHOUT_PADDING } from "./theme/constants"; + +export type KurtosisBreadcrumbsHandle = { + crumb?: (state: RemoveFunctions, params: Params) => KurtosisBreadcrumb | KurtosisBreadcrumb[]; + extraControls?: (state: RemoveFunctions, params: Params) => ReactElement | null; +}; type KurtosisBreadcrumbMenuItem = { name: string; @@ -33,51 +39,89 @@ export type KurtosisBreadcrumb = { }; export const KurtosisBreadcrumbs = () => { - const { enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave } = useEmuiAppContext(); + const { enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave, starlarkRunningInEnclaves } = + useEmuiAppContext(); - const matches = useMatches() as UIMatch< - object, - { - crumb?: ( - state: RemoveFunctions, - params: Params, - ) => KurtosisBreadcrumb | Promise; - } - >[]; + const matches = useMatches() as UIMatch[]; - const [matchCrumbs, setMatchCrumbs] = useState([]); + const matchCrumbs = useMemo( + () => + matches.flatMap((match) => { + if (isDefined(match.handle?.crumb)) { + const r = match.handle.crumb( + { + enclaves, + filesAndArtifactsByEnclave, + starlarkRunsByEnclave, + servicesByEnclave, + starlarkRunningInEnclaves, + }, + match.params, + ); + return Array.isArray(r) ? r : [r]; + } + return []; + }), + [ + matches, + enclaves, + filesAndArtifactsByEnclave, + starlarkRunsByEnclave, + servicesByEnclave, + starlarkRunningInEnclaves, + ], + ); - useEffect(() => { - (async () => { - setMatchCrumbs( - await Promise.all( - matches - .map((match) => - isDefined(match.handle?.crumb) - ? Promise.resolve( - match.handle.crumb( - { enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave }, - match.params, - ), - ) - : null, - ) - .filter(isDefined), - ), - ); - })(); - }, [matches, enclaves, filesAndArtifactsByEnclave, starlarkRunsByEnclave, servicesByEnclave]); + const extraControls = useMemo( + () => + matches + .map((match) => + isDefined(match.handle?.extraControls) + ? match.handle?.extraControls( + { + enclaves, + filesAndArtifactsByEnclave, + starlarkRunsByEnclave, + servicesByEnclave, + starlarkRunningInEnclaves, + }, + match.params, + ) + : null, + ) + .filter(isDefined), + [ + matches, + enclaves, + filesAndArtifactsByEnclave, + starlarkRunsByEnclave, + servicesByEnclave, + starlarkRunningInEnclaves, + ], + ); return ( - - }> - {matchCrumbs.map((crumb, i, arr) => ( - - - - ))} - -   + + + + + / + + } + > + {matchCrumbs.map((crumb, i, arr) => ( + + + + ))} + +   + + {extraControls} + ); }; @@ -88,12 +132,16 @@ type KurtosisBreadcrumbItemProps = KurtosisBreadcrumb & { const KurtosisBreadcrumbItem = ({ name, destination, alternatives, isLastItem }: KurtosisBreadcrumbItemProps) => { if (isLastItem) { - return {name}; + return ( + + {name} + + ); } const baseLink = ( - @@ -109,8 +157,8 @@ const KurtosisBreadcrumbItem = ({ name, destination, alternatives, isLastItem }: as={IconButton} variant={"breadcrumb"} aria-label={"Other options"} - icon={} - size={"sm"} + icon={} + size={"xs"} /> {alternatives.map(({ name, destination, icon }) => ( diff --git a/enclave-manager/web/src/components/KurtosisThemeProvider.tsx b/enclave-manager/web/src/components/KurtosisThemeProvider.tsx index a498bdbcdb..069503dd5f 100644 --- a/enclave-manager/web/src/components/KurtosisThemeProvider.tsx +++ b/enclave-manager/web/src/components/KurtosisThemeProvider.tsx @@ -50,6 +50,7 @@ const theme = extendTheme({ }, gray: { 100: "#E3E3E3", // text + 150: "#A1A3A5", 200: "#878787", 250: "#7A7A7A", 300: "#606770", @@ -59,6 +60,7 @@ const theme = extendTheme({ 650: "#292929", 700: "#1E1E1E", 800: "#1D1D1D", // selected background + 850: "#1B1B1D", 900: "#111111", // ui background }, }, @@ -77,6 +79,7 @@ const theme = extendTheme({ }, "nav.primaryNav": { bg: mode(props.theme.semanticTokens.colors["chakra-body-bg"]._light, "black")(props), + zIndex: "1", }, main: { color: "gray.100", @@ -130,6 +133,7 @@ const theme = extendTheme({ })), ghost: defineStyle((props) => ({ _hover: { bg: "gray.650" }, + color: props.colorScheme === "gray" ? undefined : `${props.colorScheme}.400`, })), sortableHeader: (props: StyleFunctionProps) => { const ghost = theme.components.Button.variants!.ghost(props); @@ -139,12 +143,20 @@ const theme = extendTheme({ textTransform: "uppercase", }; }, + fileTree: (props: StyleFunctionProps) => { + const ghost = theme.components.Button.variants!.ghost(props); + return { + ...ghost, + width: "100%", + fontWeight: "medium", + justifyContent: "flex-start", + }; + }, breadcrumb: (props: StyleFunctionProps) => { const ghost = theme.components.Button.variants!.ghost(props); return { ...ghost, color: "gray.100", - fontWeight: "normal", }; }, nav: { @@ -177,11 +189,43 @@ const theme = extendTheme({ }, }, Card: { - baseStyle: { - container: { - bg: "gray.800", - borderRadius: "8px", - padding: "16px", + variants: { + valueCard: { + container: { + bg: "gray.800", + borderRadius: "8px", + padding: "16px", + gap: "16px", + }, + header: { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + padding: "0px", + }, + body: { + padding: "0px", + }, + }, + titledCard: { + container: { + height: "100%", + bgColor: "none", + borderColor: "gray.500", + borderStyle: "solid", + borderWidth: "1px", + borderRadius: "6px", + overflow: "clip", + }, + header: { + bg: "gray.850", + padding: "12px", + }, + body: { + padding: "6px 12px", + height: "100%", + width: "100%", + }, }, }, }, @@ -237,15 +281,24 @@ const theme = extendTheme({ }, })), }, - Table: { variants: { simple: { + tr: { + _notLast: { + borderBottom: "1px solid", + borderColor: "whiteAlpha.300", + }, + }, th: { color: "gray.100", - borderBottom: "1px solid", - borderColor: "gray.500", + backgroundColor: "gray.850", textTransform: "uppercase", + borderBottom: "1px solid", + borderColor: "whiteAlpha.300", + }, + td: { + borderBottom: "none", }, }, }, diff --git a/enclave-manager/web/src/components/TitledBox.tsx b/enclave-manager/web/src/components/TitledBox.tsx new file mode 100644 index 0000000000..9c0eadb196 --- /dev/null +++ b/enclave-manager/web/src/components/TitledBox.tsx @@ -0,0 +1,19 @@ +import { Flex, Text } from "@chakra-ui/react"; +import { PropsWithChildren } from "react"; + +type TitledBoxProps = PropsWithChildren<{ + title: string; +}>; + +export const TitledBox = ({ title, children }: TitledBoxProps) => { + return ( + + + + {title} + + + {children} + + ); +}; diff --git a/enclave-manager/web/src/components/TitledCard.tsx b/enclave-manager/web/src/components/TitledCard.tsx index a6fd074dc0..1d06d01680 100644 --- a/enclave-manager/web/src/components/TitledCard.tsx +++ b/enclave-manager/web/src/components/TitledCard.tsx @@ -1,19 +1,34 @@ -import { Card, Flex, Text } from "@chakra-ui/react"; -import { PropsWithChildren } from "react"; +import { Card, CardBody, CardHeader, CardProps, Flex, Text } from "@chakra-ui/react"; +import { PropsWithChildren, ReactElement } from "react"; -type TitledCardProps = PropsWithChildren<{ - title: string; -}>; +type TitledCardProps = CardProps & + PropsWithChildren<{ + title: string; + controls?: ReactElement; + rightControls?: ReactElement; + }>; -export const TitledCard = ({ title, children }: TitledCardProps) => { +export const TitledCard = ({ title, controls, rightControls, children, ...cardProps }: TitledCardProps) => { return ( - - - - {title} - - - {children} + + + + + {title} + + {controls} + + {rightControls} + + {children} ); }; diff --git a/enclave-manager/web/src/components/ValueCard.tsx b/enclave-manager/web/src/components/ValueCard.tsx index b9d30a2e54..a2af182457 100644 --- a/enclave-manager/web/src/components/ValueCard.tsx +++ b/enclave-manager/web/src/components/ValueCard.tsx @@ -1,4 +1,4 @@ -import { Card, Flex, Text } from "@chakra-ui/react"; +import { Card, CardBody, CardHeader, Text } from "@chakra-ui/react"; import { ReactElement } from "react"; import { isDefined } from "../utils"; import { CopyButton } from "./CopyButton"; @@ -12,8 +12,8 @@ type ValueCardProps = { export const ValueCard = ({ title, value, copyEnabled, copyValue }: ValueCardProps) => { return ( - - + + {title} @@ -23,10 +23,12 @@ export const ValueCard = ({ title, value, copyEnabled, copyValue }: ValueCardPro contentName={title} /> )} - - - {value} - + + + + {value} + + ); }; diff --git a/enclave-manager/web/src/components/enclaves/CreateEnclaveButton.tsx b/enclave-manager/web/src/components/enclaves/CreateEnclaveButton.tsx index 9bd92f5d5e..69ff7244ed 100644 --- a/enclave-manager/web/src/components/enclaves/CreateEnclaveButton.tsx +++ b/enclave-manager/web/src/components/enclaves/CreateEnclaveButton.tsx @@ -13,7 +13,7 @@ export const CreateEnclaveButton = () => { as={Button} colorScheme={"kurtosisGreen"} leftIcon={} - size={"md"} + size={"sm"} onClick={() => navigate(`#${KURTOSIS_CREATE_ENCLAVE_URL_ARG}`)} > New Enclave diff --git a/enclave-manager/web/src/components/enclaves/EditEnclaveButton.tsx b/enclave-manager/web/src/components/enclaves/EditEnclaveButton.tsx index 25f1119f07..e7db575ae1 100644 --- a/enclave-manager/web/src/components/enclaves/EditEnclaveButton.tsx +++ b/enclave-manager/web/src/components/enclaves/EditEnclaveButton.tsx @@ -1,4 +1,4 @@ -import { Button, Tooltip } from "@chakra-ui/react"; +import { Button, ButtonProps, Tooltip } from "@chakra-ui/react"; import { useState } from "react"; import { FiEdit2 } from "react-icons/fi"; import { KurtosisPackage } from "../../client/packageIndexer/api/kurtosis_package_indexer_pb"; @@ -7,11 +7,11 @@ import { isDefined } from "../../utils"; import { ConfigureEnclaveModal } from "./modals/ConfigureEnclaveModal"; import { PackageLoadingModal } from "./modals/PackageLoadingModal"; -type EditEnclaveButtonProps = { +type EditEnclaveButtonProps = ButtonProps & { enclave: EnclaveFullInfo; }; -export const EditEnclaveButton = ({ enclave }: EditEnclaveButtonProps) => { +export const EditEnclaveButton = ({ enclave, ...buttonProps }: EditEnclaveButtonProps) => { const [showPackageLoader, setShowPackageLoader] = useState(false); const [kurtosisPackage, setKurtosisPackage] = useState(); @@ -22,7 +22,7 @@ export const EditEnclaveButton = ({ enclave }: EditEnclaveButtonProps) => { if (!isDefined(enclave.starlarkRun)) { return ( - ); @@ -31,7 +31,7 @@ export const EditEnclaveButton = ({ enclave }: EditEnclaveButtonProps) => { if (enclave.starlarkRun.isErr) { return ( - @@ -44,7 +44,13 @@ export const EditEnclaveButton = ({ enclave }: EditEnclaveButtonProps) => { label={"Edit this enclave. From here you can edit the enclave configuration and update it."} openDelay={1000} > - diff --git a/enclave-manager/web/src/components/enclaves/GotToEncalaveOverviewButton.tsx b/enclave-manager/web/src/components/enclaves/GotToEncalaveOverviewButton.tsx new file mode 100644 index 0000000000..29fabc1edf --- /dev/null +++ b/enclave-manager/web/src/components/enclaves/GotToEncalaveOverviewButton.tsx @@ -0,0 +1,22 @@ +import { Button } from "@chakra-ui/react"; +import { IoExitOutline } from "react-icons/io5"; +import { Link } from "react-router-dom"; +import { isDefined } from "../../utils"; + +type GotToEncalaveOverviewButtonProps = { + enclaveUUID?: string; +}; + +export const GoToEnclaveOverviewButton = ({ enclaveUUID }: GotToEncalaveOverviewButtonProps) => { + if (!isDefined(enclaveUUID)) { + return null; + } + + return ( + + + + ); +}; diff --git a/enclave-manager/web/src/components/enclaves/configuration/inputs/DictArgumentInput.tsx b/enclave-manager/web/src/components/enclaves/configuration/inputs/DictArgumentInput.tsx index fa9edac5c0..6a78177bbd 100644 --- a/enclave-manager/web/src/components/enclaves/configuration/inputs/DictArgumentInput.tsx +++ b/enclave-manager/web/src/components/enclaves/configuration/inputs/DictArgumentInput.tsx @@ -40,7 +40,6 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg JSON.stringify( getValues(otherProps.name).reduce( @@ -50,7 +49,7 @@ export const DictArgumentInput = ({ keyType, valueType, ...otherProps }: DictArg ) } /> - + {fields.map((field, i) => ( diff --git a/enclave-manager/web/src/components/enclaves/configuration/inputs/ListArgumentInput.tsx b/enclave-manager/web/src/components/enclaves/configuration/inputs/ListArgumentInput.tsx index 8818ac9d04..111a7ce120 100644 --- a/enclave-manager/web/src/components/enclaves/configuration/inputs/ListArgumentInput.tsx +++ b/enclave-manager/web/src/components/enclaves/configuration/inputs/ListArgumentInput.tsx @@ -38,7 +38,6 @@ export const ListArgumentInput = ({ valueType, ...otherProps }: ListArgumentInpu JSON.stringify(getValues(otherProps.name).map(({ value }: { value: any }) => value))} /> diff --git a/enclave-manager/web/src/components/enclaves/logs/LogLine.tsx b/enclave-manager/web/src/components/enclaves/logs/LogLine.tsx index 2c0398f803..57da6b2bed 100644 --- a/enclave-manager/web/src/components/enclaves/logs/LogLine.tsx +++ b/enclave-manager/web/src/components/enclaves/logs/LogLine.tsx @@ -1,38 +1,22 @@ import { Box, Flex } from "@chakra-ui/react"; + +import Convert from "ansi-to-html"; import parse from "html-react-parser"; -import { DateTime } from "luxon"; -import { isDefined } from "../../../utils"; -// @ts-ignore -import hasAnsi from "has-ansi"; import { ReactElement } from "react"; -import { normalizeLogText } from "./LogViewer"; +import { hasAnsi, isDefined } from "../../../utils"; +import { LogLineMessage, LogStatus } from "./types"; +import { normalizeLogText } from "./utils"; -const Convert = require("ansi-to-html"); const convert = new Convert(); -export type LogStatus = "info" | "error"; - -export type LogLineProps = { - timestamp?: DateTime; - message?: string; - status?: LogStatus; -}; - -export type LogLineSearch = { - searchTerm: string; - pattern: RegExp; -}; - -export type LogLineInput = { - logLineProps: LogLineProps; - logLineSearch?: LogLineSearch; - selected: boolean | undefined; +type LogLineProps = LogLineMessage & { + highlightPattern?: RegExp; + selected?: boolean; }; const logFontFamily = "Menlo, Monaco, Inconsolata, Consolas, Courier, monospace"; -export const LogLine = ({ logLineProps, logLineSearch, selected }: LogLineInput) => { - const { timestamp, message, status } = logLineProps; +export const LogLine = ({ timestamp, message, status, highlightPattern, selected }: LogLineProps) => { const statusToColor = (status?: LogStatus) => { switch (status) { case "error": @@ -44,56 +28,6 @@ export const LogLine = ({ logLineProps, logLineSearch, selected }: LogLineInput) } }; - const processText = (text: string, selected: boolean | undefined) => { - let reactComponent; - if (hasAnsi(text)) { - reactComponent = parse(convert.toHtml(text)); - } else { - reactComponent = <>{text}; - } - - if (logLineSearch) { - reactComponent = HighlightPattern({ text, regex: logLineSearch.pattern, selected }); - } - return reactComponent; - }; - - const HighlightPattern = ({ - text, - regex, - selected, - }: { - text: string; - regex: RegExp; - selected: boolean | undefined; - }) => { - const normalizedLogText = normalizeLogText(text); - const splitText = normalizedLogText.split(regex); - const matches = normalizedLogText.match(regex); - - if (!isDefined(matches)) { - return {text}; - } - - return ( - - {splitText.reduce( - (arr: (ReactElement | string)[], element, index) => - matches[index] - ? [ - ...arr, - element, - - {matches[index]} - , - ] - : [...arr, element], - [], - )} - - ); - }; - return ( {isDefined(timestamp) && ( @@ -121,8 +55,45 @@ export const LogLine = ({ logLineProps, logLineSearch, selected }: LogLineInput) color={statusToColor(status)} _focus={{ boxShadow: "outline" }} > - {message && processText(message, selected)} + ); }; + +type MessageProps = { + message?: string; + highlightPattern?: RegExp; +}; + +const Message = ({ message, highlightPattern }: MessageProps) => { + if (!isDefined(message)) { + return null; + } + + if (hasAnsi(message)) { + return <>{parse(convert.toHtml(message))}; + } + + if (highlightPattern) { + const normalizedLogText = normalizeLogText(message); + const splitText = normalizedLogText.split(highlightPattern); + const matches = normalizedLogText.match(highlightPattern); + + if (!isDefined(matches)) { + return {message}; + } + + return ( + + {splitText.reduce( + (arr: (ReactElement | string)[], element, index) => + matches[index] ? [...arr, element, {matches[index]}] : [...arr, element], + [], + )} + + ); + } + + return <>{message}; +}; diff --git a/enclave-manager/web/src/components/enclaves/logs/LogViewer.tsx b/enclave-manager/web/src/components/enclaves/logs/LogViewer.tsx index 3e5acb310d..0a84cecc03 100644 --- a/enclave-manager/web/src/components/enclaves/logs/LogViewer.tsx +++ b/enclave-manager/web/src/components/enclaves/logs/LogViewer.tsx @@ -1,6 +1,5 @@ import { SmallCloseIcon } from "@chakra-ui/icons"; import { - Box, Button, ButtonGroup, Editable, @@ -8,10 +7,12 @@ import { EditablePreview, Flex, FormControl, + FormErrorMessage, FormLabel, - HStack, + Icon, Input, InputGroup, + InputLeftElement, InputRightElement, Progress, Switch, @@ -20,73 +21,62 @@ import { } from "@chakra-ui/react"; import { debounce, throttle } from "lodash"; import { ChangeEvent, MutableRefObject, ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { FiSearch } from "react-icons/fi"; +import { MdArrowBackIosNew, MdArrowForwardIos } from "react-icons/md"; import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; -import { isDefined, isNotEmpty, stripAnsi } from "../../../utils"; +import { isDefined, isNotEmpty, stringifyError, stripAnsi } from "../../../utils"; import { CopyButton } from "../../CopyButton"; import { DownloadButton } from "../../DownloadButton"; -import { LogLine, LogLineProps, LogLineSearch } from "./LogLine"; +import { FindCommand } from "../../KeyboardCommands"; +import { LogLine } from "./LogLine"; +import { LogLineMessage } from "./types"; +import { normalizeLogText } from "./utils"; type LogViewerProps = { - logLines: LogLineProps[]; + logLines: LogLineMessage[]; progressPercent?: number | "indeterminate" | "failed"; ProgressWidget?: ReactElement; logsFileName?: string; + searchEnabled?: boolean; }; -export const normalizeLogText = (rawText: string) => { - return rawText.trim(); +type SearchBaseState = { + rawSearchTerm: string; }; +type SearchInitState = SearchBaseState & { + type: "init"; +}; + +type SearchErrorState = SearchBaseState & { + type: "error"; + error: string; +}; + +type SearchSuccessState = SearchBaseState & { + type: "success"; + pattern: RegExp; + searchMatchesIndices: number[]; + currentSearchIndex?: number; +}; + +type SearchState = SearchInitState | SearchErrorState | SearchSuccessState; + export const LogViewer = ({ progressPercent, logLines: propsLogLines, ProgressWidget, logsFileName, + searchEnabled, }: LogViewerProps) => { const virtuosoRef = useRef(null); const [logLines, setLogLines] = useState(propsLogLines); const [userIsScrolling, setUserIsScrolling] = useState(false); const [automaticScroll, setAutomaticScroll] = useState(true); - const throttledSetLogLines = useMemo(() => throttle(setLogLines, 500), []); - const searchRef: MutableRefObject = useRef(null); - const [search, setSearch] = useState(undefined); - const [rawSearchTerm, setRawSearchTerm] = useState(""); - const [searchMatchesIndices, setSearchMatchesIndices] = useState([]); - const [currentSearchIndex, setCurrentSearchIndex] = useState(undefined); + const [searchState, setSearchState] = useState({ type: "init", rawSearchTerm: "" }); - useEffect(() => { - window.addEventListener("keydown", function (e) { - const element = searchRef?.current; - if ((e.ctrlKey && e.keyCode === 70) || (e.metaKey && e.keyCode === 70)) { - if (element !== document.activeElement) { - e.preventDefault(); - element?.focus(); - } - } - // Next search match with cmd/ctrl+G - // if ((e.ctrlKey && e.keyCode === 71) || (e.metaKey && e.keyCode === 71)) { - // console.log("NEXT", e.keyCode); - // e.preventDefault(); - // nextMatch(); - // } - - // Clear the search on escape - if (e.key === "Escape" || e.keyCode === 27) { - if (element === document.activeElement) { - e.preventDefault(); - setSearch(undefined); - setRawSearchTerm(""); - setSearchMatchesIndices([]); - setCurrentSearchIndex(undefined); - } - } - }); - }, []); - - useEffect(() => { - throttledSetLogLines(propsLogLines); - }, [propsLogLines, throttledSetLogLines]); + const throttledSetLogLines = useMemo(() => throttle(setLogLines, 500), []); const handleAutomaticScrollChange = (e: ChangeEvent) => { setAutomaticScroll(e.target.checked); @@ -103,6 +93,20 @@ export const LogViewer = ({ } }; + const handleSearchStateChange = (updater: ((prevState: SearchState) => SearchState) | SearchState) => { + setSearchState((prevState) => { + const newState = typeof updater === "object" ? updater : updater(prevState); + if ( + newState.type === "success" && + (prevState.type !== "success" || prevState.currentSearchIndex !== newState.currentSearchIndex) && + isDefined(newState.currentSearchIndex) + ) { + virtuosoRef.current?.scrollToIndex(newState.searchMatchesIndices[newState.currentSearchIndex]); + } + return newState; + }); + }; + const getLogsValue = () => { return logLines .map(({ message }) => message) @@ -111,190 +115,49 @@ export const LogViewer = ({ .join("\n"); }; - useEffect(() => { - if (search) findMatches(search); - }, [search?.searchTerm, logLines]); - - const updateSearchTerm = (rawText: string) => { - setCurrentSearchIndex(undefined); - const searchTerm = normalizeLogText(rawText); - const logLineSearch: LogLineSearch = { - searchTerm: searchTerm, - pattern: new RegExp(searchTerm, "gi"), // `i` is invariant case - }; - setSearch(logLineSearch); - }; - const debouncedUpdateSearchTerm = debounce(updateSearchTerm, 100); - const debouncedUpdateSearchTermCallback = useCallback(debouncedUpdateSearchTerm, []); - - const hasSearchTerm = () => { - if (!search) return false; - return isDefined(search.searchTerm) && isNotEmpty(search.searchTerm); - }; - - const findMatches = (search: LogLineSearch) => { - setSearchMatchesIndices([]); - if (hasSearchTerm()) { - const matches = logLines.flatMap((line, index) => { - if (line?.message && normalizeLogText(line.message).match(search.pattern)) { - return index; - } else { - return []; - } - }); - setSearchMatchesIndices(matches); - } - }; - - const handleOnChange = (e: ChangeEvent) => { - setRawSearchTerm(e.target.value); - debouncedUpdateSearchTermCallback(e.target.value); - }; - - const priorMatch = () => { - if (searchMatchesIndices.length > 0) { - const newIndex = isDefined(currentSearchIndex) ? currentSearchIndex - 1 : 0; - updateSearchIndexBounded(newIndex); - } - }; - - const nextMatch = () => { - if (searchMatchesIndices.length > 0) { - const newIndex = isDefined(currentSearchIndex) ? currentSearchIndex + 1 : 0; - updateSearchIndexBounded(newIndex); - } - }; - - const updateSearchIndexBounded = (newIndex: number) => { - if (newIndex > searchMatchesIndices.length - 1) { - newIndex = 0; - } - if (newIndex < 0) { - newIndex = searchMatchesIndices.length - 1; - } - setCurrentSearchIndex(newIndex); - return newIndex; - }; - - useEffect(() => { - if (virtuosoRef?.current && currentSearchIndex !== undefined && currentSearchIndex >= 0) { - virtuosoRef.current.scrollToIndex(searchMatchesIndices[currentSearchIndex]); - } - }, [currentSearchIndex]); - - const clearSearch = () => { - setRawSearchTerm(""); - setSearch(undefined); - setSearchMatchesIndices([]); - setCurrentSearchIndex(undefined); - }; - - const parseMatchIndexRequest = (input: string) => { - let parsed = parseInt(input); - if (isNaN(parsed) || parsed < 1) return 1; - if (parsed > searchMatchesIndices.length) return searchMatchesIndices.length; - return parsed; - }; - - const highlight = (currentSearchIndex: number | undefined, thisIndex: number, searchableIndices: number[]) => { + const isIndexSelected = (index: number) => { return ( - currentSearchIndex !== undefined && - searchableIndices.length > 0 && - searchableIndices[currentSearchIndex] === thisIndex + searchState.type === "success" && + isDefined(searchState.currentSearchIndex) && + searchState.searchMatchesIndices[searchState.currentSearchIndex] === index ); }; + useEffect(() => { + throttledSetLogLines(propsLogLines); + }, [propsLogLines, throttledSetLogLines]); + return ( - - - - - - - - {rawSearchTerm && ( - - - - )} - - - - - {hasSearchTerm() && ( - - - - <> - {searchMatchesIndices.length > 0 && currentSearchIndex !== undefined && ( - <> - - updateSearchIndexBounded(parseMatchIndexRequest(inputString) - 1) - } - > - - - - - - <>/ - - )} - <>{searchMatchesIndices.length} matches - - - - - )} - - - {isDefined(ProgressWidget) && ( - - {ProgressWidget} - + + + {searchEnabled && ( + )} + {isDefined(ProgressWidget) && ProgressWidget} + + isDefined(message))} itemContent={(index, line) => ( )} /> @@ -307,23 +170,259 @@ export const LogViewer = ({ /> )} - + - - + + Automatic Scroll - - + + ); }; + +type SearchControlsProps = { + searchState: SearchState; + onChangeSearchState: (update: ((oldSearchState: SearchState) => SearchState) | SearchState) => void; + logLines: LogLineMessage[]; +}; + +const SearchControls = ({ searchState, onChangeSearchState, logLines }: SearchControlsProps) => { + const searchRef: MutableRefObject = useRef(null); + const [showSearchForm, setShowSearchForm] = useState(false); + + const updateMatches = useCallback( + (searchTerm: string) => { + if (isNotEmpty(searchTerm)) { + try { + const pattern = new RegExp(searchTerm, "gi"); // i is case insensitive + const matches = logLines + .map((line, index) => { + if (line?.message && normalizeLogText(line.message).match(pattern)) { + return index; + } + return null; + }) + .filter(isDefined); + onChangeSearchState((state) => ({ + type: "success", + rawSearchTerm: state.rawSearchTerm, + pattern, + searchMatchesIndices: matches, + currentSearchIndex: matches.length > 0 ? 0 : undefined, + })); + } catch (error: any) { + onChangeSearchState((state) => ({ + type: "error", + rawSearchTerm: state.rawSearchTerm, + error: stringifyError(error), + })); + } + } else { + onChangeSearchState((state) => ({ type: "init", rawSearchTerm: state.rawSearchTerm })); + } + }, + [logLines, onChangeSearchState], + ); + + const debouncedUpdateMatches = useMemo(() => debounce(updateMatches, 100), [updateMatches]); + + const handleOnChange = (e: ChangeEvent) => { + onChangeSearchState((state) => ({ ...state, rawSearchTerm: e.target.value })); + debouncedUpdateMatches(e.target.value); + }; + + const updateSearchIndexBounded = (newIndex: number) => { + if (searchState.type !== "success") { + return; + } + if (newIndex > searchState.searchMatchesIndices.length - 1) { + newIndex = 0; + } + if (newIndex < 0) { + newIndex = searchState.searchMatchesIndices.length - 1; + } + onChangeSearchState((state) => ({ ...state, currentSearchIndex: newIndex })); + }; + + const handlePriorMatchClick = () => { + updateSearchIndexBounded( + searchState.type === "success" && isDefined(searchState.currentSearchIndex) + ? searchState.currentSearchIndex - 1 + : 0, + ); + }; + + const handleNextMatchClick = () => { + updateSearchIndexBounded( + searchState.type === "success" && isDefined(searchState.currentSearchIndex) + ? searchState.currentSearchIndex + 1 + : 0, + ); + }; + + const handleClearSearch = () => { + onChangeSearchState({ type: "init", rawSearchTerm: "" }); + }; + + const handleIndexInputChange = (text: string) => { + if (searchState.type !== "success") { + return; + } + let index = parseInt(text); + if (isNaN(index)) { + index = 1; + } + if (index > searchState.searchMatchesIndices.length) { + index = searchState.searchMatchesIndices.length; + } + updateSearchIndexBounded(index - 1); + }; + + useEffect(() => { + const listener = function (e: KeyboardEvent) { + const element = searchRef?.current; + if ((e.ctrlKey && e.keyCode === 70) || (e.metaKey && e.keyCode === 70)) { + setShowSearchForm(true); + if (element !== document.activeElement) { + e.preventDefault(); + element?.focus(); + } + } + // Next search match with cmd/ctrl+G + // if ((e.ctrlKey && e.keyCode === 71) || (e.metaKey && e.keyCode === 71)) { + // console.log("NEXT", e.keyCode); + // e.preventDefault(); + // nextMatch(); + // } + + // Clear the search on escape + if (e.key === "Escape" || e.keyCode === 27) { + if (element === document.activeElement) { + e.preventDefault(); + onChangeSearchState({ type: "init", rawSearchTerm: "" }); + } + } + }; + window.addEventListener("keydown", listener); + return () => window.removeEventListener("keydown", listener); + }, [onChangeSearchState, searchRef]); + + if (!showSearchForm) { + return ( + + ); + } else { + return ( + + + + + + + + {searchState.type !== "init" && ( + + + + )} + + + + + + {searchState.rawSearchTerm.length > 0 && ( + + {searchState.type === "success" && ( + + {searchState.searchMatchesIndices.length > 0 && searchState.currentSearchIndex !== undefined && ( + + + + + + + + <>/ + + )} + {searchState.searchMatchesIndices.length} matches + + )} + + )} + + {searchState.type === "error" && {searchState.error}} + + ); + } +}; diff --git a/enclave-manager/web/src/components/enclaves/logs/types.ts b/enclave-manager/web/src/components/enclaves/logs/types.ts new file mode 100644 index 0000000000..2185f13065 --- /dev/null +++ b/enclave-manager/web/src/components/enclaves/logs/types.ts @@ -0,0 +1,9 @@ +import { DateTime } from "luxon"; + +export type LogStatus = "info" | "error"; + +export type LogLineMessage = { + status?: LogStatus; + message?: string; + timestamp?: DateTime; +}; diff --git a/enclave-manager/web/src/components/enclaves/logs/utils.ts b/enclave-manager/web/src/components/enclaves/logs/utils.ts new file mode 100644 index 0000000000..1bea1c70aa --- /dev/null +++ b/enclave-manager/web/src/components/enclaves/logs/utils.ts @@ -0,0 +1,3 @@ +export const normalizeLogText = (rawText: string) => { + return rawText.trim(); +}; diff --git a/enclave-manager/web/src/components/enclaves/modals/ConfigureEnclaveModal.tsx b/enclave-manager/web/src/components/enclaves/modals/ConfigureEnclaveModal.tsx index 18b53faa91..e7d6313860 100644 --- a/enclave-manager/web/src/components/enclaves/modals/ConfigureEnclaveModal.tsx +++ b/enclave-manager/web/src/components/enclaves/modals/ConfigureEnclaveModal.tsx @@ -81,8 +81,6 @@ export const ConfigureEnclaveModal = ({ return isDefined(value) ? `${value}` : ""; case ArgumentValueType.STRING: return value || ""; - case ArgumentValueType.JSON: - return isDefined(value) ? JSON.stringify(value) : "{}"; case ArgumentValueType.LIST: assertDefined(innerType1, `Cannot parse a list argument type without knowing innerType1`); return isDefined(value) ? value.map((v: any) => convertArgValue(innerType1, v)) : []; @@ -177,7 +175,7 @@ export const ConfigureEnclaveModal = ({ return; } - let apicInfo = existingEnclave?.apiContainerInfo; + let enclave = existingEnclave; let enclaveUUID = existingEnclave?.shortenedUuid; if (!isDefined(existingEnclave)) { setIsLoading(true); @@ -192,12 +190,12 @@ export const ConfigureEnclaveModal = ({ setError(`Did not receive enclave info when running createEnclave`); return; } - apicInfo = newEnclave.value.enclaveInfo.apiContainerInfo; + enclave = newEnclave.value.enclaveInfo; enclaveUUID = newEnclave.value.enclaveInfo.shortenedUuid; } - if (!isDefined(apicInfo)) { - setError(`Cannot trigger starlark run as apic info cannot be found`); + if (!isDefined(enclave)) { + setError(`Cannot trigger starlark run as enclave info cannot be found`); return; } @@ -217,9 +215,13 @@ export const ConfigureEnclaveModal = ({ } console.log("submissionData for runStarlarkPackage", submissionData); - const logsIterator = await runStarlarkPackage(apicInfo, kurtosisPackage.name, submissionData); - navigator(`/enclave/${enclaveUUID}/logs`, { state: { logs: logsIterator } }); - onClose(); + try { + const logsIterator = await runStarlarkPackage(enclave, kurtosisPackage.name, submissionData); + navigator(`/enclave/${enclaveUUID}/logs`, { state: { logs: logsIterator } }); + onClose(); + } catch (error: any) { + setError(stringifyError(error)); + } }; return ( diff --git a/enclave-manager/web/src/components/enclaves/tables/FilesTable.tsx b/enclave-manager/web/src/components/enclaves/tables/FilesTable.tsx index b0a0ebe40d..ce68236757 100644 --- a/enclave-manager/web/src/components/enclaves/tables/FilesTable.tsx +++ b/enclave-manager/web/src/components/enclaves/tables/FilesTable.tsx @@ -1,9 +1,11 @@ +import { Button } from "@chakra-ui/react"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { FilesArtifactNameAndUuid, ListFilesArtifactNamesAndUuidsResponse, } from "enclave-manager-sdk/build/api_container_service_pb"; import { useMemo } from "react"; +import { Link } from "react-router-dom"; import { EnclaveFullInfo } from "../../../emui/enclaves/types"; import { RemoveFunctions } from "../../../utils/types"; import { DataTable } from "../../DataTable"; @@ -21,12 +23,13 @@ export const FilesTable = ({ filesAndArtifacts, enclave }: FilesTableProps) => { () => [ columnHelper.accessor("fileName", { header: "Name", - cell: ({ row, getValue }) => - // - // - // + cell: ({ row, getValue }) => ( + + + + ), }), columnHelper.display({ id: "download", diff --git a/enclave-manager/web/src/components/enclaves/widgets/DeleteEnclavesButton.tsx b/enclave-manager/web/src/components/enclaves/widgets/DeleteEnclavesButton.tsx index fdad79b818..539a70ae9e 100644 --- a/enclave-manager/web/src/components/enclaves/widgets/DeleteEnclavesButton.tsx +++ b/enclave-manager/web/src/components/enclaves/widgets/DeleteEnclavesButton.tsx @@ -1,4 +1,4 @@ -import { Button, Tooltip } from "@chakra-ui/react"; +import { Button, ButtonProps, Tooltip } from "@chakra-ui/react"; import { useState } from "react"; import { FiTrash2 } from "react-icons/fi"; import { useNavigate } from "react-router-dom"; @@ -6,11 +6,11 @@ import { useEmuiAppContext } from "../../../emui/EmuiAppContext"; import { EnclaveFullInfo } from "../../../emui/enclaves/types"; import { KurtosisAlertModal } from "../../KurtosisAlertModal"; -type DeleteEnclavesButtonProps = { +type DeleteEnclavesButtonProps = ButtonProps & { enclaves: EnclaveFullInfo[]; }; -export const DeleteEnclavesButton = ({ enclaves }: DeleteEnclavesButtonProps) => { +export const DeleteEnclavesButton = ({ enclaves, ...buttonProps }: DeleteEnclavesButtonProps) => { const { destroyEnclaves } = useEmuiAppContext(); const navigator = useNavigate(); @@ -28,7 +28,13 @@ export const DeleteEnclavesButton = ({ enclaves }: DeleteEnclavesButtonProps) => return ( <> - diff --git a/enclave-manager/web/src/components/theme/constants.ts b/enclave-manager/web/src/components/theme/constants.ts index 192a91fd0f..968973524f 100644 --- a/enclave-manager/web/src/components/theme/constants.ts +++ b/enclave-manager/web/src/components/theme/constants.ts @@ -1,4 +1,13 @@ import * as CSS from "csstype"; +export const BREADCRUMBS_HEIGHT = "76px"; + +export const MAIN_APP_TOP_PADDING = "24px"; +export const MAIN_APP_BOTTOM_PADDING = "20px"; +export const MAIN_APP_LEFT_PADDING = "112px"; +export const MAIN_APP_RIGHT_PADDING = "40px"; + export const MAIN_APP_MAX_WIDTH: CSS.Property.MaxWidth | number = "1320px"; +export const MAIN_APP_MAX_WIDTH_WITHOUT_PADDING: CSS.Property.MaxWidth | number = `${1320 - 112 - 40}px`; + export const FLEX_STANDARD_GAP: CSS.Property.Gap | number = "32px"; diff --git a/enclave-manager/web/src/components/theme/tabsTheme.ts b/enclave-manager/web/src/components/theme/tabsTheme.ts index 2525409de1..6945d67bac 100644 --- a/enclave-manager/web/src/components/theme/tabsTheme.ts +++ b/enclave-manager/web/src/components/theme/tabsTheme.ts @@ -6,34 +6,43 @@ const { defineMultiStyleConfig } = createMultiStyleConfigHelpers(tabsAnatomy.key // export the component theme export const tabsTheme = defineMultiStyleConfig({ defaultProps: { - variant: "soft-rounded", + variant: "line", colorScheme: "kurtosisGreen", }, variants: { - "soft-rounded": (props: StyleFunctionProps) => ({ + line: (props: StyleFunctionProps) => ({ root: { + display: "flex", + flexDirection: "column", height: "100%", + width: "100%", + flex: "1", + }, + tablist: { + height: "47px", + borderColor: "transparent", }, tab: { - fontStyle: "normal", - fontWeight: "medium", - fontSize: "lg", + fontWeight: "md", + fontSize: "sm", color: "gray.100", lineHeight: "28px", - _hover: { - bg: `gray.700`, - }, - _selected: { - fontWeight: "semibold", - color: `${props.colorScheme}.400`, - bg: `gray.800`, + padding: "4px 16px 2px 16px", + _active: { + bg: "none", }, textTransform: "capitalize", }, tabpanels: { + display: "flex", + flexDirection: "column", height: "100%", + flex: "1", }, tabpanel: { + display: "flex", + flexDirection: "column", + flex: "1", padding: "32px 0px", height: "100%", }, diff --git a/enclave-manager/web/src/emui/App.tsx b/enclave-manager/web/src/emui/App.tsx index 1e638c0f70..dff686686e 100644 --- a/enclave-manager/web/src/emui/App.tsx +++ b/enclave-manager/web/src/emui/App.tsx @@ -11,7 +11,6 @@ import { KurtosisThemeProvider } from "../components/KurtosisThemeProvider"; import { catalogRoutes } from "./catalog/CatalogRoutes"; import { EmuiAppContextProvider } from "./EmuiAppContext"; import { enclaveRoutes } from "./enclaves/EnclaveRoutes"; -import { Navbar } from "./Navbar"; const logLogo = (t: string) => console.log(`%c ${t}`, "background: black; color: #00C223"); logLogo(` @@ -60,7 +59,7 @@ const KurtosisRouter = () => { [ { element: ( - }> + diff --git a/enclave-manager/web/src/emui/EmuiAppContext.tsx b/enclave-manager/web/src/emui/EmuiAppContext.tsx index 627e5ea692..b42eadc8c1 100644 --- a/enclave-manager/web/src/emui/EmuiAppContext.tsx +++ b/enclave-manager/web/src/emui/EmuiAppContext.tsx @@ -6,11 +6,7 @@ import { ListFilesArtifactNamesAndUuidsResponse, StarlarkRunResponseLine, } from "enclave-manager-sdk/build/api_container_service_pb"; -import { - CreateEnclaveResponse, - EnclaveAPIContainerInfo, - EnclaveInfo, -} from "enclave-manager-sdk/build/engine_service_pb"; +import { CreateEnclaveResponse, EnclaveInfo } from "enclave-manager-sdk/build/engine_service_pb"; import { createContext, PropsWithChildren, @@ -23,7 +19,7 @@ import { } from "react"; import { Result } from "true-myth"; import { useKurtosisClient } from "../client/enclaveManager/KurtosisClientContext"; -import { isDefined } from "../utils"; +import { assertDefined, isDefined } from "../utils"; import { RemoveFunctions } from "../utils/types"; import { EnclaveFullInfo } from "./enclaves/types"; @@ -32,6 +28,7 @@ export type EmuiAppState = { servicesByEnclave: Record>; filesAndArtifactsByEnclave: Record>; starlarkRunsByEnclave: Record>; + starlarkRunningInEnclaves: RemoveFunctions[]; // Methods refreshEnclaves: () => Promise[], string>>; @@ -48,25 +45,14 @@ export type EmuiAppState = { ) => Promise>; destroyEnclaves: (enclaveUUIDs: string[]) => Promise[]>; runStarlarkPackage: ( - apicInfo: RemoveFunctions, + enclave: RemoveFunctions, packageId: string, args: Record, ) => Promise>; + updateStarlarkFinishedInEnclave: (enclave: RemoveFunctions) => void; }; -const EmuiAppContext = createContext({ - enclaves: Result.err("Enclaves not initialised, call refreshEnclaves"), - servicesByEnclave: {}, - filesAndArtifactsByEnclave: {}, - starlarkRunsByEnclave: {}, - refreshEnclaves: () => null as any, - refreshServices: () => null as any, - refreshFilesAndArtifacts: () => null as any, - refreshStarlarkRun: () => null as any, - createEnclave: () => null as any, - destroyEnclaves: () => null as any, - runStarlarkPackage: () => null as any, -}); +const EmuiAppContext = createContext(null as any); export const EmuiAppContextProvider = ({ children }: PropsWithChildren) => { const [isInitialLoading, setIsInitialLoading] = useState(true); @@ -76,6 +62,7 @@ export const EmuiAppContextProvider = ({ children }: PropsWithChildren) => { servicesByEnclave: {}, filesAndArtifactsByEnclave: {}, starlarkRunsByEnclave: {}, + starlarkRunningInEnclaves: [], }); const kurtosisClient = useKurtosisClient(); @@ -178,14 +165,24 @@ export const EmuiAppContextProvider = ({ children }: PropsWithChildren) => { ); const runStarlarkPackage = useCallback( - async (apicInfo: RemoveFunctions, packageId: string, args: Record) => { - const resp = await kurtosisClient.runStarlarkPackage(apicInfo, packageId, args); - // TODO: Proxy lines to build optimistic ui + async (enclave: RemoveFunctions, packageId: string, args: Record) => { + setState((state) => ({ ...state, starlarkRunningInEnclaves: [...state.starlarkRunningInEnclaves, enclave] })); + assertDefined(enclave.apiContainerInfo, `apic info not defined in enclave ${enclave.name}`); + const resp = await kurtosisClient.runStarlarkPackage(enclave.apiContainerInfo, packageId, args); return resp; }, [kurtosisClient], ); + const updateStarlarkFinishedInEnclave = useCallback((enclave: RemoveFunctions) => { + setState((state) => ({ + ...state, + starlarkRunningInEnclaves: state.starlarkRunningInEnclaves.filter( + (runningEnclave) => runningEnclave.enclaveUuid !== enclave.enclaveUuid, + ), + })); + }, []); + useEffect(() => { (async () => { await refreshEnclaves(); @@ -215,6 +212,7 @@ export const EmuiAppContextProvider = ({ children }: PropsWithChildren) => { createEnclave, destroyEnclaves, runStarlarkPackage, + updateStarlarkFinishedInEnclave, }} > {children} @@ -243,6 +241,23 @@ export const useFullEnclave = (enclaveUUID: string): Result>(() => { + if (!isDefined(enclave)) { + return Result.err(`Could not find enclave ${enclaveUUID}`); + } + + if (enclaves.isErr) { + return enclaves.cast(); + } + + return Result.ok({ + ...enclave, + services, + filesAndArtifacts, + starlarkRun, + }); + }, [enclaveUUID, enclaves, enclave, services, filesAndArtifacts, starlarkRun]); + useEffect(() => { if (isDefined(enclave) && !isDefined(services)) { refreshServices(enclave); @@ -261,20 +276,7 @@ export const useFullEnclave = (enclaveUUID: string): Result => { diff --git a/enclave-manager/web/src/emui/enclaves/EnclaveList.tsx b/enclave-manager/web/src/emui/enclaves/EnclaveList.tsx index 2a74456fa3..92d5315170 100644 --- a/enclave-manager/web/src/emui/enclaves/EnclaveList.tsx +++ b/enclave-manager/web/src/emui/enclaves/EnclaveList.tsx @@ -1,5 +1,6 @@ -import { Button, ButtonGroup, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { Button, ButtonGroup, Flex, Text } from "@chakra-ui/react"; import { useEffect, useMemo, useState } from "react"; +import { AppPageLayout } from "../../components/AppLayout"; import { CreateEnclaveButton } from "../../components/enclaves/CreateEnclaveButton"; import { EnclavesTable } from "../../components/enclaves/tables/EnclavesTable"; import { DeleteEnclavesButton } from "../../components/enclaves/widgets/DeleteEnclavesButton"; @@ -28,37 +29,33 @@ export const EnclaveList = () => { }, [enclavesKey]); return ( - - - - - Enclaves - - - {selectedEnclaves.length > 0 && ( - - - - - )} - - + + + + Enclaves + + + {selectedEnclaves.length > 0 && ( + + + + + )} + - - - {enclaves.isOk && ( - - )} - {enclaves.isErr && } - - - - + + + {enclaves.isOk && ( + + )} + {enclaves.isErr && } + + ); }; diff --git a/enclave-manager/web/src/emui/enclaves/EnclaveRoutes.tsx b/enclave-manager/web/src/emui/enclaves/EnclaveRoutes.tsx index 525fb596fa..000553324c 100644 --- a/enclave-manager/web/src/emui/enclaves/EnclaveRoutes.tsx +++ b/enclave-manager/web/src/emui/enclaves/EnclaveRoutes.tsx @@ -1,15 +1,25 @@ import { Icon } from "@chakra-ui/react"; -import { ServiceInfo } from "enclave-manager-sdk/build/api_container_service_pb"; +import { FilesArtifactNameAndUuid, ServiceInfo } from "enclave-manager-sdk/build/api_container_service_pb"; import { FiPlus } from "react-icons/fi"; -import { Params, RouteObject } from "react-router-dom"; +import { Outlet, Params, RouteObject } from "react-router-dom"; import { KurtosisClient } from "../../client/enclaveManager/KurtosisClient"; +import { GoToEnclaveOverviewButton } from "../../components/enclaves/GotToEncalaveOverviewButton"; +import { KurtosisBreadcrumbsHandle } from "../../components/KurtosisBreadcrumbs"; import { RemoveFunctions } from "../../utils/types"; import { EmuiAppState } from "../EmuiAppContext"; +import { Artifact } from "./enclave/artifact/Artifact"; import { Enclave } from "./enclave/Enclave"; +import { EnclaveRouteContextProvider } from "./enclave/EnclaveRouteContext"; +import { EnclaveLogs } from "./enclave/logs/EnclaveLogs"; import { Service } from "./enclave/service/Service"; import { EnclaveList } from "./EnclaveList"; -export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => [ +type KurtosisRouteObject = RouteObject & { + handle?: KurtosisBreadcrumbsHandle; + children?: KurtosisRouteObject[]; +}; + +export const enclaveRoutes = (kurtosisClient: KurtosisClient): KurtosisRouteObject[] => [ { path: "/enclaves?", handle: { crumb: () => ({ name: "Enclaves", destination: "/" }) }, @@ -23,8 +33,13 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => { path: "/enclave/:enclaveUUID", id: "enclave", + element: ( + + + + ), handle: { - crumb: async ({ enclaves: enclavesResult }: RemoveFunctions, params: Params) => { + crumb: ({ enclaves: enclavesResult }: RemoveFunctions, params: Params) => { const enclaves = enclavesResult.unwrapOr([]); const enclave = enclaves.find((enclave) => enclave.shortenedUuid === params.enclaveUUID); return { @@ -33,6 +48,7 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => alternatives: [ ...enclaves .filter((enclave) => enclave.shortenedUuid !== params.enclaveUUID) + .sort((a, b) => a.name.localeCompare(b.name)) .map((enclave) => ({ name: enclave.name, destination: `/enclave/${enclave.shortenedUuid}`, @@ -45,12 +61,13 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => ], }; }, + hasTabs: true, }, children: [ { path: "service/:serviceUUID", handle: { - crumb: async ({ servicesByEnclave }: RemoveFunctions, params: Params) => { + crumb: ({ servicesByEnclave }: RemoveFunctions, params: Params) => { const services = Object.values( servicesByEnclave[params.enclaveUUID || ""]?.unwrapOr({ serviceInfo: {} as Record, @@ -64,12 +81,14 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => destination: `/enclave/${params.enclaveUUID}/service/${params.serviceUUID}`, alternatives: services .filter((service) => service.shortenedUuid !== params.serviceUUID) + .sort((a, b) => a.name.localeCompare(b.name)) .map((service) => ({ name: service.name, destination: `/enclave/${params.enclaveUUID}/service/${service.shortenedUuid}`, })), }; }, + hasTabs: true, }, children: [ { @@ -95,6 +114,53 @@ export const enclaveRoutes = (kurtosisClient: KurtosisClient): RouteObject[] => }, { path: "file/:fileUUID", + element: , + handle: { + crumb: ({ filesAndArtifactsByEnclave }: RemoveFunctions, params: Params) => { + const artifacts = Object.values( + filesAndArtifactsByEnclave[params.enclaveUUID || ""]?.unwrapOr({ + fileNamesAndUuids: [] as FilesArtifactNameAndUuid[], + }).fileNamesAndUuids || [], + ); + const artifact = artifacts.find((artifact) => artifact.fileUuid === params.fileUUID); + const fileName = artifact?.fileName || "Unknown"; + + return [ + { + name: fileName, + destination: `/enclave/${params.enclaveUUID}/file/${params.fileUUID}`, + alternatives: artifacts + .filter((artifact) => artifact.fileUuid !== params.fileUUID) + .sort((a, b) => a.fileName.localeCompare(b.fileName)) + .map((artifact) => ({ + name: artifact.fileName, + destination: `/enclave/${params.enclaveUUID}/file/${artifact.fileUuid}`, + })), + }, + { name: "Files", destination: `/enclave/${params.enclaveUUID}/file/${params.fileUUID}` }, + ]; + }, + hasTabs: false, + extraControls: (state: RemoveFunctions, params: Params) => ( + + ), + }, + }, + { + path: "logs", + id: "enclaveLogs", + element: , + handle: { + hasTabs: false, + extraControls: ({ starlarkRunningInEnclaves }: RemoveFunctions, params: Params) => + starlarkRunningInEnclaves.some((enclave) => enclave.shortenedUuid === params.enclaveUUID) ? null : ( + + ), + crumb: () => ({ + name: "Logs", + destination: "none", + }), + }, }, { path: ":activeTab?", diff --git a/enclave-manager/web/src/emui/enclaves/enclave/Enclave.tsx b/enclave-manager/web/src/emui/enclaves/enclave/Enclave.tsx index ee053eba89..803cd79bfb 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/Enclave.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/Enclave.tsx @@ -1,21 +1,19 @@ -import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; -import { Location, useLocation, useNavigate, useParams } from "react-router-dom"; +import { Flex, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react"; +import { useNavigate, useParams } from "react-router-dom"; -import { StarlarkRunResponseLine } from "enclave-manager-sdk/build/api_container_service_pb"; -import { FunctionComponent, useEffect, useState } from "react"; +import { FunctionComponent, useState } from "react"; +import { AppPageLayout } from "../../../components/AppLayout"; import { EditEnclaveButton } from "../../../components/enclaves/EditEnclaveButton"; import { DeleteEnclavesButton } from "../../../components/enclaves/widgets/DeleteEnclavesButton"; import { FeatureNotImplementedModal } from "../../../components/FeatureNotImplementedModal"; +import { HoverLineTabList } from "../../../components/HoverLineTabList"; import { KurtosisAlert } from "../../../components/KurtosisAlert"; -import { isDefined } from "../../../utils"; import { useFullEnclave } from "../../EmuiAppContext"; import { EnclaveFullInfo } from "../types"; -import { EnclaveLogs } from "./logs/EnclaveLogs"; import { EnclaveOverview } from "./overview/EnclaveOverview"; const tabs: { path: string; element: FunctionComponent<{ enclave: EnclaveFullInfo }> }[] = [ { path: "overview", element: EnclaveOverview }, - { path: "logs", element: EnclaveLogs }, ]; export const Enclave = () => { @@ -23,7 +21,11 @@ export const Enclave = () => { const enclave = useFullEnclave(enclaveUUID || "unknown"); if (enclave.isErr) { - return ; + return ( + + + + ); } return ; @@ -36,7 +38,6 @@ type EnclaveImplProps = { const EnclaveImpl = ({ enclave }: EnclaveImplProps) => { const navigator = useNavigate(); const params = useParams(); - const location = useLocation() as Location<{ logs: AsyncIterable }>; const activeTab = params.activeTab || "overview"; const activeIndex = tabs.findIndex((tab) => tab.path === activeTab); @@ -46,41 +47,31 @@ const EnclaveImpl = ({ enclave }: EnclaveImplProps) => { const handleTabChange = (newTabIndex: number) => { const tab = tabs[newTabIndex]; - if (tab.path === "logs" && !isDefined(location.state?.logs)) { - setUnavailableModalState({ - isOpen: true, - featureName: "Enclave Logs", - issueUrl: "https://github.com/kurtosis-tech/kurtosis/issues/1721", - message: - "Enclave logs are currently only viewable during configuration. Please upvote this feature request if you'd like enclave logs to be persisted.", - }); - return; - } navigator(`/enclave/${enclave.shortenedUuid}/${tab.path}`); }; - useEffect(() => { - if (isDefined(location.state?.logs)) { - navigator(`/enclave/${enclave.shortenedUuid}/logs`, { state: location.state, replace: true }); - } - }, [navigator, location.state, activeIndex, enclave.shortenedUuid]); - return ( - - - - - - {tabs.map((tab) => ( - {tab.path} - ))} - - - - - + + + + + + {enclave.name} + + path)} activeTab={activeTab} /> + + + + - + setUnavailableModalState({ isOpen: false })} + /> + {tabs.map((tab) => ( @@ -88,14 +79,7 @@ const EnclaveImpl = ({ enclave }: EnclaveImplProps) => { ))} - - setUnavailableModalState({ isOpen: false })} - /> - + + ); }; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/EnclaveRouteContext.tsx b/enclave-manager/web/src/emui/enclaves/enclave/EnclaveRouteContext.tsx new file mode 100644 index 0000000000..c54b2e57c1 --- /dev/null +++ b/enclave-manager/web/src/emui/enclaves/enclave/EnclaveRouteContext.tsx @@ -0,0 +1,32 @@ +import { createContext, PropsWithChildren, useContext } from "react"; +import { useParams } from "react-router-dom"; +import { AppPageLayout } from "../../../components/AppLayout"; +import { KurtosisAlert } from "../../../components/KurtosisAlert"; +import { useFullEnclave } from "../../EmuiAppContext"; +import { EnclaveFullInfo } from "../types"; + +type EnclaveRouteContextState = { + enclave: EnclaveFullInfo; +}; + +const EnclaveRouteContext = createContext({ enclave: null as any }); + +export const EnclaveRouteContextProvider = ({ children }: PropsWithChildren) => { + const { enclaveUUID } = useParams(); + const enclave = useFullEnclave(enclaveUUID || "Unknown"); + + if (enclave.isErr) { + return ( + + + + ); + } + + return {children}; +}; + +export const useEnclaveFromParams = () => { + const { enclave } = useContext(EnclaveRouteContext); + return enclave; +}; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/artifact/Artifact.tsx b/enclave-manager/web/src/emui/enclaves/enclave/artifact/Artifact.tsx new file mode 100644 index 0000000000..96861e824f --- /dev/null +++ b/enclave-manager/web/src/emui/enclaves/enclave/artifact/Artifact.tsx @@ -0,0 +1,195 @@ +import { Button, ButtonGroup, Flex, Spinner } from "@chakra-ui/react"; +import { InspectFilesArtifactContentsResponse } from "enclave-manager-sdk/build/api_container_service_pb"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { BiPaintRoll } from "react-icons/bi"; +import { useParams } from "react-router-dom"; +import { Result } from "true-myth"; +import { useKurtosisClient } from "../../../../client/enclaveManager/KurtosisClientContext"; +import { AppPageLayout } from "../../../../components/AppLayout"; +import { CodeEditor, CodeEditorImperativeAttributes } from "../../../../components/CodeEditor"; +import { CopyButton } from "../../../../components/CopyButton"; +import { DownloadButton } from "../../../../components/DownloadButton"; +import { FileTree, FileTreeNode } from "../../../../components/FileTree"; +import { KurtosisAlert } from "../../../../components/KurtosisAlert"; +import { TitledCard } from "../../../../components/TitledCard"; +import { isDefined } from "../../../../utils"; +import { useFullEnclave } from "../../../EmuiAppContext"; +import { EnclaveFullInfo } from "../../types"; + +export const Artifact = () => { + const { fileUUID, enclaveUUID } = useParams(); + + if (!isDefined(fileUUID) || !isDefined(enclaveUUID)) { + return ( + + + + ); + } + + return ; +}; + +type ArtifactLoaderProps = { + enclaveUUID: string; + fileUUID: string; +}; + +const ArtifactLoader = ({ enclaveUUID, fileUUID }: ArtifactLoaderProps) => { + const [filesResult, setFilesResult] = useState>(); + + const enclave = useFullEnclave(enclaveUUID); + const kurtosisClient = useKurtosisClient(); + + useEffect(() => { + (async () => { + if (enclave.isOk) { + setFilesResult(undefined); + const files = await kurtosisClient.inspectFilesArtifactContents(enclave.value, fileUUID); + setFilesResult(files); + } + })(); + }, [kurtosisClient, enclave, fileUUID]); + + if (!isDefined(filesResult)) { + return ( + + + + ); + } + + if (filesResult.isErr) { + return ( + + + + ); + } + + if (enclave.isErr) { + return ( + + + + ); + } + + const artifactName = + enclave.value.filesAndArtifacts?.mapOr( + undefined, + (files) => files.fileNamesAndUuids.find((file) => file.fileUuid === fileUUID)?.fileUuid, + ) || "Unknown"; + + return ; +}; + +type ArtifactImplProps = { + enclave: EnclaveFullInfo; + artifactName: string; + files: InspectFilesArtifactContentsResponse; +}; + +const ArtifactImpl = ({ enclave, artifactName, files }: ArtifactImplProps) => { + const codeEditorRef = useRef(null); + const [selectedFilePath, setSelectedFilePath] = useState(); + + const filesAsFileTree = useMemo(() => { + return files.fileDescriptions + .filter((fileDescription) => !fileDescription.path.endsWith("/")) + .reduce( + (acc, fileDescription): FileTreeNode => { + const filePath = fileDescription.path.split("/"); + let destinationNode = acc; + let i = 0; + while (i < filePath.length - 1) { + const filePart = filePath[i]; + let nextNode = destinationNode.childNodes?.find((node) => node.name === filePart); + if (!isDefined(nextNode)) { + nextNode = { name: filePart, childNodes: [] }; + destinationNode.childNodes?.push(nextNode); + } + destinationNode = nextNode; + i++; + } + destinationNode.childNodes?.push({ + name: filePath[filePath.length - 1], + size: fileDescription.size, + }); + + return acc; + }, + { name: "root", childNodes: [] } as FileTreeNode, + ); + }, [files]); + + const selectedFile = useMemo(() => { + const path = selectedFilePath?.join("/"); + return files.fileDescriptions.find((file) => file.path === path); + }, [files, selectedFilePath]); + + return ( + + + + + + + + + + + + ) : undefined + } + rightControls={ + isDefined(selectedFile) ? ( + + ) : undefined + } + flex={"1"} + minH={"100%"} + > + {isDefined(selectedFile) && isDefined(selectedFilePath) && ( + + )} + + + + ); +}; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/logs/EnclaveLogs.tsx b/enclave-manager/web/src/emui/enclaves/enclave/logs/EnclaveLogs.tsx index 29d506a7f6..4caf997248 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/logs/EnclaveLogs.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/logs/EnclaveLogs.tsx @@ -1,13 +1,16 @@ -import { CircularProgress, Icon } from "@chakra-ui/react"; +import { ButtonGroup, CircularProgress, Flex, Icon, Tag } from "@chakra-ui/react"; import { StarlarkRunResponseLine } from "enclave-manager-sdk/build/api_container_service_pb"; import { useEffect, useState } from "react"; import { FiCheck, FiX } from "react-icons/fi"; import { Location, useLocation, useNavigate } from "react-router-dom"; -import { LogLineProps } from "../../../../components/enclaves/logs/LogLine"; +import { AppPageLayout } from "../../../../components/AppLayout"; +import { EditEnclaveButton } from "../../../../components/enclaves/EditEnclaveButton"; import { LogViewer } from "../../../../components/enclaves/logs/LogViewer"; +import { LogLineMessage } from "../../../../components/enclaves/logs/types"; +import { DeleteEnclavesButton } from "../../../../components/enclaves/widgets/DeleteEnclavesButton"; import { isAsyncIterable, stringifyError } from "../../../../utils"; import { useEmuiAppContext } from "../../../EmuiAppContext"; -import { EnclaveFullInfo } from "../../types"; +import { useEnclaveFromParams } from "../EnclaveRouteContext"; // These are the stages we want to catch and handle in the UI type EnclaveLogStage = @@ -19,7 +22,7 @@ type EnclaveLogStage = const LOG_STARTING_EXECUTION = "Starting execution"; -export function starlarkResponseLineToLogLineProps(l: StarlarkRunResponseLine): LogLineProps { +export function starlarkResponseLineToLogLineMessage(l: StarlarkRunResponseLine): LogLineMessage { switch (l.runResponseLine.case) { case "instruction": return { message: l.runResponseLine.value.executableInstruction }; @@ -38,16 +41,14 @@ export function starlarkResponseLineToLogLineProps(l: StarlarkRunResponseLine): } } -type EnclaveLogsProps = { - enclave: EnclaveFullInfo; -}; - -export const EnclaveLogs = ({ enclave }: EnclaveLogsProps) => { - const { refreshServices, refreshFilesAndArtifacts, refreshStarlarkRun } = useEmuiAppContext(); +export const EnclaveLogs = () => { + const enclave = useEnclaveFromParams(); + const { refreshServices, refreshFilesAndArtifacts, refreshStarlarkRun, updateStarlarkFinishedInEnclave } = + useEmuiAppContext(); const navigator = useNavigate(); const location = useLocation() as Location<{ logs: AsyncIterable }>; const [progress, setProgress] = useState({ stage: "waiting" }); - const [logLines, setLogLines] = useState([]); + const [logLines, setLogLines] = useState([]); useEffect(() => { let cancelled = false; @@ -60,7 +61,7 @@ export const EnclaveLogs = ({ enclave }: EnclaveLogsProps) => { if (cancelled) { return; } - const parsedLine = starlarkResponseLineToLogLineProps(line); + const parsedLine = starlarkResponseLineToLogLineMessage(line); setLogLines((logLines) => [...logLines, parsedLine]); setProgress((oldProgress) => { if (line.runResponseLine.case === "progressInfo") { @@ -98,6 +99,8 @@ export const EnclaveLogs = ({ enclave }: EnclaveLogsProps) => { } setLogLines((logLines) => [...logLines, { message: `Error: ${stringifyError(error)}`, status: "error" }]); await Promise.all([refreshStarlarkRun(enclave), refreshServices(enclave), refreshFilesAndArtifacts(enclave)]); + } finally { + updateStarlarkFinishedInEnclave(enclave); } } else { navigator(`/enclave/${enclave.shortenedUuid}/overview`); @@ -121,12 +124,22 @@ export const EnclaveLogs = ({ enclave }: EnclaveLogsProps) => { : 0; return ( - } - logsFileName={`${enclave.name.replaceAll(/\s+/g, "_")}-logs.txt`} - /> + + + + + + + + + } + logsFileName={`${enclave.name.replaceAll(/\s+/g, "_")}-logs.txt`} + /> + ); }; @@ -136,35 +149,37 @@ type ProgressSummaryProps = { const ProgressSummary = ({ progress }: ProgressSummaryProps) => { return ( - <> - {progress.stage === "waiting" && "Waiting"} - {progress.stage === "validating" && "Validating"} - {progress.stage === "executing" && ( - <> - - - {progress.step} / {progress.totalSteps} - - - )} - {progress.stage === "done" && ( - <> - - - {progress.totalSteps} / {progress.totalSteps} - - - )} - {progress.stage === "failed" && ( - <> - - Failed - - )} - + + + {progress.stage === "waiting" && "Waiting"} + {progress.stage === "validating" && "Validating"} + {progress.stage === "executing" && ( + <> + + + {progress.step} / {progress.totalSteps} + + + )} + {progress.stage === "done" && ( + <> + + + {progress.totalSteps} / {progress.totalSteps} + + + )} + {progress.stage === "failed" && ( + <> + + Failed + + )} + + ); }; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/overview/EnclaveOverview.tsx b/enclave-manager/web/src/emui/enclaves/enclave/overview/EnclaveOverview.tsx index bdc8fd61a8..11a85332ad 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/overview/EnclaveOverview.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/overview/EnclaveOverview.tsx @@ -6,7 +6,7 @@ import { EnclaveStatus } from "../../../../components/enclaves/widgets/EnclaveSt import { FormatDateTime } from "../../../../components/FormatDateTime"; import { KurtosisAlert } from "../../../../components/KurtosisAlert"; import { FLEX_STANDARD_GAP } from "../../../../components/theme/constants"; -import { TitledCard } from "../../../../components/TitledCard"; +import { TitledBox } from "../../../../components/TitledBox"; import { ValueCard } from "../../../../components/ValueCard"; import { isDefined } from "../../../../utils"; import { EnclaveFullInfo } from "../../types"; @@ -47,14 +47,14 @@ export const EnclaveOverview = ({ enclave }: EnclaveOverviewProps) => { /> - + {!isDefined(enclave.services) && } {isDefined(enclave.services) && enclave.services.isOk && ( )} {isDefined(enclave.services) && enclave.services.isErr && } - - + + {!isDefined(enclave.filesAndArtifacts) && } {isDefined(enclave.filesAndArtifacts) && enclave.filesAndArtifacts.isOk && ( @@ -62,7 +62,7 @@ export const EnclaveOverview = ({ enclave }: EnclaveOverviewProps) => { {isDefined(enclave.filesAndArtifacts) && enclave.filesAndArtifacts.isErr && ( )} - + ); }; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/service/Service.tsx b/enclave-manager/web/src/emui/enclaves/enclave/service/Service.tsx index 2f8aa10ce4..b7e5e9f2ac 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/service/Service.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/service/Service.tsx @@ -1,11 +1,13 @@ -import { Flex, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { Flex, Spinner, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react"; import { ServiceInfo } from "enclave-manager-sdk/build/api_container_service_pb"; import { FunctionComponent } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { AppPageLayout } from "../../../../components/AppLayout"; +import { HoverLineTabList } from "../../../../components/HoverLineTabList"; import { KurtosisAlert } from "../../../../components/KurtosisAlert"; import { isDefined } from "../../../../utils"; -import { useFullEnclave } from "../../../EmuiAppContext"; import { EnclaveFullInfo } from "../../types"; +import { useEnclaveFromParams } from "../EnclaveRouteContext"; import { ServiceLogs } from "./logs/ServiceLogs"; import { ServiceOverview } from "./overview/ServiceOverview"; @@ -15,29 +17,37 @@ const tabs: { path: string; element: FunctionComponent<{ enclave: EnclaveFullInf ]; export const Service = () => { - const { enclaveUUID, serviceUUID } = useParams(); - const enclave = useFullEnclave(enclaveUUID || "unknown"); + const { serviceUUID } = useParams(); + const enclave = useEnclaveFromParams(); - if (enclave.isErr) { - return ; + if (!isDefined(enclave.services)) { + return ( + + + + ); } - if (!isDefined(enclave.value.services)) { - return ; + if (enclave.services.isErr) { + return ( + + + + ); } - if (enclave.value.services.isErr) { - return ; - } - - const service = Object.values(enclave.value.services.value.serviceInfo).find( + const service = Object.values(enclave.services.value.serviceInfo).find( (service) => service.shortenedUuid === serviceUUID, ); if (!isDefined(service)) { - return ; + return ( + + + + ); } - return ; + return ; }; type ServiceImplProps = { @@ -57,15 +67,14 @@ const ServiceImpl = ({ enclave, service }: ServiceImplProps) => { }; return ( - - - - - {tabs.map((tab) => ( - {tab.path} - ))} - - + + + + + {service.name} + + path)} activeTab={activeTab} /> + {tabs.map((tab) => ( @@ -73,7 +82,7 @@ const ServiceImpl = ({ enclave, service }: ServiceImplProps) => { ))} - - + + ); }; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/service/logs/ServiceLogs.tsx b/enclave-manager/web/src/emui/enclaves/enclave/service/logs/ServiceLogs.tsx index 0fa43f9606..678884f426 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/service/logs/ServiceLogs.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/service/logs/ServiceLogs.tsx @@ -3,12 +3,12 @@ import { ServiceInfo } from "enclave-manager-sdk/build/api_container_service_pb" import { DateTime } from "luxon"; import { useEffect, useState } from "react"; import { useKurtosisClient } from "../../../../../client/enclaveManager/KurtosisClientContext"; -import { LogLineProps } from "../../../../../components/enclaves/logs/LogLine"; import { LogViewer } from "../../../../../components/enclaves/logs/LogViewer"; +import { LogLineMessage } from "../../../../../components/enclaves/logs/types"; import { isDefined } from "../../../../../utils"; import { EnclaveFullInfo } from "../../../types"; -const serviceLogLineToLogLineProps = (lines: string[], timestamp?: Timestamp): LogLineProps[] => { +const serviceLogLineToLogLineMessage = (lines: string[], timestamp?: Timestamp): LogLineMessage[] => { return lines.map((line) => ({ message: line, timestamp: isDefined(timestamp) ? DateTime.fromJSDate(timestamp?.toDate()) : undefined, @@ -40,7 +40,7 @@ export async function reTryCatch( export const ServiceLogs = ({ enclave, service }: ServiceLogsProps) => { const kurtosisClient = useKurtosisClient(); - const [logLines, setLogLines] = useState([]); + const [logLines, setLogLines] = useState([]); useEffect(() => { let canceled = false; @@ -55,7 +55,7 @@ export const ServiceLogs = ({ enclave, service }: ServiceLogsProps) => { if (canceled) return; const lineGroupForService = lineGroup.serviceLogsByServiceUuid[service.serviceUuid]; if (!isDefined(lineGroupForService)) continue; - const parsedLines = serviceLogLineToLogLineProps(lineGroupForService.line, lineGroupForService.timestamp); + const parsedLines = serviceLogLineToLogLineMessage(lineGroupForService.line, lineGroupForService.timestamp); setLogLines((logLines) => [...logLines, ...parsedLines]); } } catch (error: any) { @@ -75,5 +75,5 @@ export const ServiceLogs = ({ enclave, service }: ServiceLogsProps) => { }, [enclave, service, kurtosisClient]); const logsFileName = `${enclave.name}--${service.name}-logs.txt`; - return ; + return ; }; diff --git a/enclave-manager/web/src/emui/enclaves/enclave/service/overview/ServiceOverview.tsx b/enclave-manager/web/src/emui/enclaves/enclave/service/overview/ServiceOverview.tsx index cec27065e2..3dcf6f95ba 100644 --- a/enclave-manager/web/src/emui/enclaves/enclave/service/overview/ServiceOverview.tsx +++ b/enclave-manager/web/src/emui/enclaves/enclave/service/overview/ServiceOverview.tsx @@ -7,7 +7,7 @@ import { ServiceStatusTag } from "../../../../../components/enclaves/widgets/Ser import { FileDisplay } from "../../../../../components/FileDisplay"; import { KurtosisAlert } from "../../../../../components/KurtosisAlert"; import { FLEX_STANDARD_GAP } from "../../../../../components/theme/constants"; -import { TitledCard } from "../../../../../components/TitledCard"; +import { TitledBox } from "../../../../../components/TitledBox"; import { ValueCard } from "../../../../../components/ValueCard"; import { isDefined } from "../../../../../utils"; import { EnclaveFullInfo } from "../../../types"; @@ -42,13 +42,13 @@ export const ServiceOverview = ({ service, enclave }: ServiceOverviewProps) => { /> - + - + {isDefined(service.container) && ( )} @@ -71,15 +71,12 @@ const ContainerOverview = ({ enclaveName, container, serviceName }: ContainerOve const entrypointJson = useMemo(() => JSON.stringify(container.entrypointArgs, undefined, 4), [container]); return ( - - - Detailed Info - - - + + + @@ -87,11 +84,11 @@ const ContainerOverview = ({ enclaveName, container, serviceName }: ContainerOve - + ); }; diff --git a/enclave-manager/web/src/utils/index.ts b/enclave-manager/web/src/utils/index.ts index 8bbd25454e..4f9997f982 100644 --- a/enclave-manager/web/src/utils/index.ts +++ b/enclave-manager/web/src/utils/index.ts @@ -34,18 +34,23 @@ export function isAsyncIterable(input: Iterable | any): input is AsyncIter return typeof input[Symbol.asyncIterator] === "function"; } +const ansiPattern = [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", +].join("|"); + +const ansiRegex = RegExp(ansiPattern, "g"); + +export function hasAnsi(text: string) { + return ansiRegex.test(text); +} + export function stripAnsi(input: string): string { if (typeof input !== "string") { throw new TypeError(`Expected a \`string\`, got \`${typeof input}\``); } - const pattern = [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", - ].join("|"); - - const re = new RegExp(pattern, "g"); - return input.replace(re, ""); + return input.replace(ansiRegex, ""); } export function range(until: number): number[]; diff --git a/enclave-manager/web/yarn.lock b/enclave-manager/web/yarn.lock index 2543bb2fb0..a412ca469c 100644 --- a/enclave-manager/web/yarn.lock +++ b/enclave-manager/web/yarn.lock @@ -5013,16 +5013,36 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" +dotenv-cli@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-6.0.0.tgz#8a30cbc59d0a8aaa166b2fee0a9a55e23a1223ab" + integrity sha512-qXlCOi3UMDhCWFKe0yq5sg3X+pJAz+RQDiFN38AMSbUrnY3uZshSfDJUAge951OS7J9gwLZGfsBlWRSOYz/TRg== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^8.0.1" + minimist "^1.2.5" + dotenv-expand@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv-expand@^8.0.1: + version "8.0.3" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e" + integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg== + dotenv@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +dotenv@^16.0.0: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -6071,13 +6091,6 @@ harmony-reflect@^1.4.6: resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== -has-ansi@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-5.0.1.tgz#eb5ce8db3465c66e0b83c7f01fd0c1ea87687071" - integrity sha512-Fp2IsZDnnyoJkKg22ZyQFvD7QRCcMTsLAtloKXyXWJ1joGLtItRU9Bv/k1o0tELL2NF3ZZBcycSKryZUM+Yl3g== - dependencies: - ansi-regex "^6.0.1" - has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -7958,7 +7971,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== diff --git a/engine/server/webapp/asset-manifest.json b/engine/server/webapp/asset-manifest.json index 8cdf429db9..6071a1d071 100644 --- a/engine/server/webapp/asset-manifest.json +++ b/engine/server/webapp/asset-manifest.json @@ -1,10 +1,10 @@ { "files": { - "main.js": "./static/js/main.bab03440.js", + "main.js": "./static/js/main.97e90b42.js", "index.html": "./index.html", - "main.bab03440.js.map": "./static/js/main.bab03440.js.map" + "main.97e90b42.js.map": "./static/js/main.97e90b42.js.map" }, "entrypoints": [ - "static/js/main.bab03440.js" + "static/js/main.97e90b42.js" ] } \ No newline at end of file diff --git a/engine/server/webapp/index.html b/engine/server/webapp/index.html index 4ca3de20c2..4d2794531a 100644 --- a/engine/server/webapp/index.html +++ b/engine/server/webapp/index.html @@ -1 +1 @@ -Kurtosis Enclave Manager
\ No newline at end of file +Kurtosis Enclave Manager
\ No newline at end of file diff --git a/engine/server/webapp/static/js/main.97e90b42.js b/engine/server/webapp/static/js/main.97e90b42.js new file mode 100644 index 0000000000..c8d1166050 --- /dev/null +++ b/engine/server/webapp/static/js/main.97e90b42.js @@ -0,0 +1,3 @@ +/*! For license information please see main.97e90b42.js.LICENSE.txt */ +!function(){var e={5304:function(e,t,n){"use strict";function r(e,t){for(var n=0;n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,s=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return u=e.done,e},e:function(e){s=!0,a=e},f:function(){try{u||null==n.return||n.return()}finally{if(s)throw a}}}}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0?40*e+55:0,u=t>0?40*t+55:0,l=n>0?40*n+55:0;r[i]=function(e){var t,n=[],r=o(e);try{for(r.s();!(t=r.n()).done;){var i=t.value;n.push(s(i))}}catch(a){r.e(a)}finally{r.f()}return"#"+n.join("")}([a,u,l])}(t,n,r,e)}))}))})),f(0,23).forEach((function(t){var n=t+232,r=s(10*t+8);e[n]="#"+r+r+r})),e}()};function s(e){for(var t=e.toString(16);t.length<2;)t="0"+t;return t}function l(e,t,n,r){var o;return"text"===t?o=function(e,t){if(t.escapeXML)return a.encodeXML(e);return e}(n,r):"display"===t?o=function(e,t,n){t=parseInt(t,10);var r,o={"-1":function(){return"
"},0:function(){return e.length&&c(e)},1:function(){return p(e,"b")},3:function(){return p(e,"i")},4:function(){return p(e,"u")},8:function(){return h(e,"display:none")},9:function(){return p(e,"strike")},22:function(){return h(e,"font-weight:normal;text-decoration:none;font-style:normal")},23:function(){return g(e,"i")},24:function(){return g(e,"u")},39:function(){return v(e,n.fg)},49:function(){return m(e,n.bg)},53:function(){return h(e,"text-decoration:overline")}};o[t]?r=o[t]():4"})).join("")}function f(e,t){for(var n=[],r=e;r<=t;r++)n.push(r);return n}function d(e){var t=null;return 0===(e=parseInt(e,10))?t="all":1===e?t="bold":2")}function h(e,t){return p(e,"span",t)}function v(e,t){return p(e,"span","color:"+t)}function m(e,t){return p(e,"span","background-color:"+t)}function g(e,t){var n;if(e.slice(-1)[0]===t&&(n=e.pop()),n)return""}var y=function(){function e(t){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),(t=t||{}).colors&&(t.colors=Object.assign({},u.colors,t.colors)),this.options=Object.assign({},u,t),this.stack=[],this.stickyStack=[]}var t,n,i;return t=e,(n=[{key:"toHtml",value:function(e){var t=this;e="string"===typeof e?[e]:e;var n=this.stack,r=this.options,i=[];return this.stickyStack.forEach((function(e){var t=l(n,e.token,e.data,r);t&&i.push(t)})),function(e,t,n){var r=!1;function i(){return""}function a(e){return t.newline?n("display",-1):n("text",e),""}var u=[{pattern:/^\x08+/,sub:i},{pattern:/^\x1b\[[012]?K/,sub:i},{pattern:/^\x1b\[\(B/,sub:i},{pattern:/^\x1b\[[34]8;2;\d+;\d+;\d+m/,sub:function(e){return n("rgb",e),""}},{pattern:/^\x1b\[38;5;(\d+)m/,sub:function(e,t){return n("xterm256Foreground",t),""}},{pattern:/^\x1b\[48;5;(\d+)m/,sub:function(e,t){return n("xterm256Background",t),""}},{pattern:/^\n/,sub:a},{pattern:/^\r+\n/,sub:a},{pattern:/^\r/,sub:a},{pattern:/^\x1b\[((?:\d{1,3};?)+|)m/,sub:function(e,t){r=!0,0===t.trim().length&&(t="0");var i,a=o(t=t.trimRight(";").split(";"));try{for(a.s();!(i=a.n()).done;){var u=i.value;n("display",u)}}catch(s){a.e(s)}finally{a.f()}return""}},{pattern:/^\x1b\[\d?J/,sub:i},{pattern:/^\x1b\[\d{0,3};\d{0,3}f/,sub:i},{pattern:/^\x1b\[?[\d;]{0,3}/,sub:i},{pattern:/^(([^\x1b\x08\r\n])+)/,sub:function(e){return n("text",e),""}}];function s(t,n){n>3&&r||(r=!1,e=e.replace(t.pattern,t.sub))}var l=[],c=e.length;e:for(;c>0;){for(var f=0,d=0,p=u.length;d0?this.children[this.children.length-1]:null},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"childNodes",{get:function(){return this.children},set:function(e){this.children=e},enumerable:!1,configurable:!0}),t}(a);t.NodeWithChildren=f;var d=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type=i.ElementType.CDATA,t}return r(t,e),Object.defineProperty(t.prototype,"nodeType",{get:function(){return 4},enumerable:!1,configurable:!0}),t}(f);t.CDATA=d;var p=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t.type=i.ElementType.Root,t}return r(t,e),Object.defineProperty(t.prototype,"nodeType",{get:function(){return 9},enumerable:!1,configurable:!0}),t}(f);t.Document=p;var h=function(e){function t(t,n,r,o){void 0===r&&(r=[]),void 0===o&&(o="script"===t?i.ElementType.Script:"style"===t?i.ElementType.Style:i.ElementType.Tag);var a=e.call(this,r)||this;return a.name=t,a.attribs=n,a.type=o,a}return r(t,e),Object.defineProperty(t.prototype,"nodeType",{get:function(){return 1},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"tagName",{get:function(){return this.name},set:function(e){this.name=e},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"attributes",{get:function(){var e=this;return Object.keys(this.attribs).map((function(t){var n,r;return{name:t,value:e.attribs[t],namespace:null===(n=e["x-attribsNamespace"])||void 0===n?void 0:n[t],prefix:null===(r=e["x-attribsPrefix"])||void 0===r?void 0:r[t]}}))},enumerable:!1,configurable:!0}),t}(f);function v(e){return(0,i.isTag)(e)}function m(e){return e.type===i.ElementType.CDATA}function g(e){return e.type===i.ElementType.Text}function y(e){return e.type===i.ElementType.Comment}function b(e){return e.type===i.ElementType.Directive}function w(e){return e.type===i.ElementType.Root}function k(e,t){var n;if(void 0===t&&(t=!1),g(e))n=new s(e.data);else if(y(e))n=new l(e.data);else if(v(e)){var r=t?x(e.children):[],i=new h(e.name,o({},e.attribs),r);r.forEach((function(e){return e.parent=i})),null!=e.namespace&&(i.namespace=e.namespace),e["x-attribsNamespace"]&&(i["x-attribsNamespace"]=o({},e["x-attribsNamespace"])),e["x-attribsPrefix"]&&(i["x-attribsPrefix"]=o({},e["x-attribsPrefix"])),n=i}else if(m(e)){r=t?x(e.children):[];var a=new d(r);r.forEach((function(e){return e.parent=a})),n=a}else if(w(e)){r=t?x(e.children):[];var u=new p(r);r.forEach((function(e){return e.parent=u})),e["x-mode"]&&(u["x-mode"]=e["x-mode"]),n=u}else{if(!b(e))throw new Error("Not implemented yet: ".concat(e.type));var f=new c(e.name,e.data);null!=e["x-name"]&&(f["x-name"]=e["x-name"],f["x-publicId"]=e["x-publicId"],f["x-systemId"]=e["x-systemId"]),n=f}return n.startIndex=e.startIndex,n.endIndex=e.endIndex,null!=e.sourceCodeLocation&&(n.sourceCodeLocation=e.sourceCodeLocation),n}function x(e){for(var t=e.map((function(e){return k(e,!0)})),n=1;n65535&&(e-=65536,t+=String.fromCharCode(e>>>10&1023|55296),e=56320|1023&e),t+=String.fromCharCode(e)};t.default=function(e){return e>=55296&&e<=57343||e>1114111?"\ufffd":(e in o.default&&(e=o.default[e]),i(e))}},2056:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.escapeUTF8=t.escape=t.encodeNonAsciiHTML=t.encodeHTML=t.encodeXML=void 0;var o=c(r(n(2586)).default),i=f(o);t.encodeXML=m(o);var a,u,s=c(r(n(9323)).default),l=f(s);function c(e){return Object.keys(e).sort().reduce((function(t,n){return t[e[n]]="&"+n+";",t}),{})}function f(e){for(var t=[],n=[],r=0,o=Object.keys(e);r1?p(e):e.charCodeAt(0)).toString(16).toUpperCase()+";"}var v=new RegExp(i.source+"|"+d.source,"g");function m(e){return function(t){return t.replace(v,(function(t){return e[t]||h(t)}))}}t.escape=function(e){return e.replace(v,h)},t.escapeUTF8=function(e){return e.replace(i,h)}},4191:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.decodeXMLStrict=t.decodeHTML5Strict=t.decodeHTML4Strict=t.decodeHTML5=t.decodeHTML4=t.decodeHTMLStrict=t.decodeHTML=t.decodeXML=t.encodeHTML5=t.encodeHTML4=t.escapeUTF8=t.escape=t.encodeNonAsciiHTML=t.encodeHTML=t.encodeXML=t.encode=t.decodeStrict=t.decode=void 0;var r=n(1298),o=n(2056);t.decode=function(e,t){return(!t||t<=0?r.decodeXML:r.decodeHTML)(e)},t.decodeStrict=function(e,t){return(!t||t<=0?r.decodeXML:r.decodeHTMLStrict)(e)},t.encode=function(e,t){return(!t||t<=0?o.encodeXML:o.encodeHTML)(e)};var i=n(2056);Object.defineProperty(t,"encodeXML",{enumerable:!0,get:function(){return i.encodeXML}}),Object.defineProperty(t,"encodeHTML",{enumerable:!0,get:function(){return i.encodeHTML}}),Object.defineProperty(t,"encodeNonAsciiHTML",{enumerable:!0,get:function(){return i.encodeNonAsciiHTML}}),Object.defineProperty(t,"escape",{enumerable:!0,get:function(){return i.escape}}),Object.defineProperty(t,"escapeUTF8",{enumerable:!0,get:function(){return i.escapeUTF8}}),Object.defineProperty(t,"encodeHTML4",{enumerable:!0,get:function(){return i.encodeHTML}}),Object.defineProperty(t,"encodeHTML5",{enumerable:!0,get:function(){return i.encodeHTML}});var a=n(1298);Object.defineProperty(t,"decodeXML",{enumerable:!0,get:function(){return a.decodeXML}}),Object.defineProperty(t,"decodeHTML",{enumerable:!0,get:function(){return a.decodeHTML}}),Object.defineProperty(t,"decodeHTMLStrict",{enumerable:!0,get:function(){return a.decodeHTMLStrict}}),Object.defineProperty(t,"decodeHTML4",{enumerable:!0,get:function(){return a.decodeHTML}}),Object.defineProperty(t,"decodeHTML5",{enumerable:!0,get:function(){return a.decodeHTML}}),Object.defineProperty(t,"decodeHTML4Strict",{enumerable:!0,get:function(){return a.decodeHTMLStrict}}),Object.defineProperty(t,"decodeHTML5Strict",{enumerable:!0,get:function(){return a.decodeHTMLStrict}}),Object.defineProperty(t,"decodeXMLStrict",{enumerable:!0,get:function(){return a.decodeXML}})},1132:function(e){"use strict";var t=Object.prototype.hasOwnProperty,n=Object.prototype.toString,r=Object.defineProperty,o=Object.getOwnPropertyDescriptor,i=function(e){return"function"===typeof Array.isArray?Array.isArray(e):"[object Array]"===n.call(e)},a=function(e){if(!e||"[object Object]"!==n.call(e))return!1;var r,o=t.call(e,"constructor"),i=e.constructor&&e.constructor.prototype&&t.call(e.constructor.prototype,"isPrototypeOf");if(e.constructor&&!o&&!i)return!1;for(r in e);return"undefined"===typeof r||t.call(e,r)},u=function(e,t){r&&"__proto__"===t.name?r(e,t.name,{enumerable:!0,configurable:!0,value:t.newValue,writable:!0}):e[t.name]=t.newValue},s=function(e,n){if("__proto__"===n){if(!t.call(e,n))return;if(o)return o(e,n).value}return e[n]};e.exports=function e(){var t,n,r,o,l,c,f=arguments[0],d=1,p=arguments.length,h=!1;for("boolean"===typeof f&&(h=f,f=arguments[1]||{},d=2),(null==f||"object"!==typeof f&&"function"!==typeof f)&&(f={});d/i,u=//i,s=function(e,t){throw new Error("This browser does not support `document.implementation.createHTMLDocument`")},l=function(e,t){throw new Error("This browser does not support `DOMParser.prototype.parseFromString`")},c="object"===typeof window&&window.DOMParser;if("function"===typeof c){var f=new c;s=l=function(e,t){return t&&(e="<".concat(t,">").concat(e,"")),f.parseFromString(e,"text/html")}}if("object"===typeof document&&document.implementation){var d=document.implementation.createHTMLDocument();s=function(e,t){if(t){var n=d.documentElement.querySelector(t);return n&&(n.innerHTML=e),d}return d.documentElement.innerHTML=e,d}}var p,h="object"===typeof document&&document.createElement("template");h&&h.content&&(p=function(e){return h.innerHTML=e,h.content.childNodes}),t.default=function(e){var t,c,f=e.match(i),d=f&&f[1]?f[1].toLowerCase():"";switch(d){case n:var h=l(e);if(!a.test(e))null===(t=null===(m=h.querySelector(r))||void 0===m?void 0:m.parentNode)||void 0===t||t.removeChild(m);if(!u.test(e))null===(c=null===(m=h.querySelector(o))||void 0===m?void 0:m.parentNode)||void 0===c||c.removeChild(m);return h.querySelectorAll(n);case r:case o:var v=s(e).querySelectorAll(d);return u.test(e)&&a.test(e)?v[0].parentNode.childNodes:v;default:return p?p(e):(m=s(e,o).querySelector(o)).childNodes;var m}}},159:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var o=r(n(9409)),i=n(1716),a=/<(![a-zA-Z\s]+)>/;t.default=function(e){if("string"!==typeof e)throw new TypeError("First argument must be a string");if(!e)return[];var t=e.match(a),n=t?t[1]:void 0;return(0,i.formatDOM)((0,o.default)(e),null,n)}},1716:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.formatDOM=t.formatAttributes=void 0;var r=n(8079),o=n(9127);function i(e){for(var t={},n=0,r=e.length;n1&&(f=v(f,{key:f.key||x})),y.push(w(f,l,x));else if("text"!==l.type){switch(d=l.attribs,s(l)?a(d.style,d):d&&(d=o(d,l.name)),p=null,l.type){case"script":case"style":l.children[0]&&(d.dangerouslySetInnerHTML={__html:l.children[0].data});break;case"tag":"textarea"===l.name&&l.children[0]?d.defaultValue=l.children[0].data:l.children&&l.children.length&&(p=e(l.children,n));break;default:continue}S>1&&(d.key=x),y.push(w(m(l.name,d,p),l,x))}else{if((c=!l.data.trim().length)&&l.parent&&!u(l.parent))continue;if(k&&c)continue;y.push(w(l.data,l,x))}return 1===y.length?y[0]:y}},4141:function(e,t,n){var r=n(2791),o=n(5792).default,i=new Set(["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"]);var a={reactCompat:!0};var u=r.version.split(".")[0]>=16,s=new Set(["tr","tbody","thead","tfoot","colgroup","table","head","html","frameset"]);e.exports={PRESERVE_CUSTOM_ATTRIBUTES:u,ELEMENTS_WITH_NO_TEXT_CHILDREN:s,isCustomComponent:function(e,t){return-1===e.indexOf("-")?t&&"string"===typeof t.is:!i.has(e)},setStyleProp:function(e,t){if(null!==e&&void 0!==e)try{t.style=o(e,a)}catch(n){t.style={}}},canTextBeChildOfNode:function(e){return!s.has(e.name)},returnFirstArg:function(e){return e}}},1065:function(e){var t=/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g,n=/\n/g,r=/^\s*/,o=/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/,i=/^:\s*/,a=/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/,u=/^[;\s]*/,s=/^\s+|\s+$/g,l="";function c(e){return e?e.replace(s,l):l}e.exports=function(e,s){if("string"!==typeof e)throw new TypeError("First argument must be a string");if(!e)return[];s=s||{};var f=1,d=1;function p(e){var t=e.match(n);t&&(f+=t.length);var r=e.lastIndexOf("\n");d=~r?e.length-r:d+e.length}function h(){var e={line:f,column:d};return function(t){return t.position=new v(e),b(),t}}function v(e){this.start=e,this.end={line:f,column:d},this.source=s.source}v.prototype.content=e;var m=[];function g(t){var n=new Error(s.source+":"+f+":"+d+": "+t);if(n.reason=t,n.filename=s.source,n.line=f,n.column=d,n.source=e,!s.silent)throw n;m.push(n)}function y(t){var n=t.exec(e);if(n){var r=n[0];return p(r),e=e.slice(r.length),n}}function b(){y(r)}function w(e){var t;for(e=e||[];t=k();)!1!==t&&e.push(t);return e}function k(){var t=h();if("/"==e.charAt(0)&&"*"==e.charAt(1)){for(var n=2;l!=e.charAt(n)&&("*"!=e.charAt(n)||"/"!=e.charAt(n+1));)++n;if(n+=2,l===e.charAt(n-1))return g("End of comment missing");var r=e.slice(2,n-2);return d+=2,p(r),e=e.slice(n),d+=2,t({type:"comment",comment:r})}}function x(){var e=h(),n=y(o);if(n){if(k(),!y(i))return g("property missing ':'");var r=y(a),s=e({type:"declaration",property:c(n[0].replace(t,l)),value:r?c(r[0].replace(t,l)):l});return y(u),s}}return b(),function(){var e,t=[];for(w(t);e=x();)!1!==e&&(t.push(e),w(t));return t}()}},6198:function(e,t,n){e=n.nmd(e);var r="__lodash_hash_undefined__",o=9007199254740991,i="[object Arguments]",a="[object AsyncFunction]",u="[object Function]",s="[object GeneratorFunction]",l="[object Null]",c="[object Object]",f="[object Proxy]",d="[object Undefined]",p=/^\[object .+?Constructor\]$/,h=/^(?:0|[1-9]\d*)$/,v={};v["[object Float32Array]"]=v["[object Float64Array]"]=v["[object Int8Array]"]=v["[object Int16Array]"]=v["[object Int32Array]"]=v["[object Uint8Array]"]=v["[object Uint8ClampedArray]"]=v["[object Uint16Array]"]=v["[object Uint32Array]"]=!0,v[i]=v["[object Array]"]=v["[object ArrayBuffer]"]=v["[object Boolean]"]=v["[object DataView]"]=v["[object Date]"]=v["[object Error]"]=v[u]=v["[object Map]"]=v["[object Number]"]=v[c]=v["[object RegExp]"]=v["[object Set]"]=v["[object String]"]=v["[object WeakMap]"]=!1;var m="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,g="object"==typeof self&&self&&self.Object===Object&&self,y=m||g||Function("return this")(),b=t&&!t.nodeType&&t,w=b&&e&&!e.nodeType&&e,k=w&&w.exports===b,x=k&&m.process,S=function(){try{var e=w&&w.require&&w.require("util").types;return e||x&&x.binding&&x.binding("util")}catch(t){}}(),T=S&&S.isTypedArray;var E,_,C=Array.prototype,I=Function.prototype,O=Object.prototype,P=y["__core-js_shared__"],N=I.toString,A=O.hasOwnProperty,D=function(){var e=/[^.]+$/.exec(P&&P.keys&&P.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),R=O.toString,j=N.call(Object),Z=RegExp("^"+N.call(A).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),F=k?y.Buffer:void 0,M=y.Symbol,L=y.Uint8Array,B=F?F.allocUnsafe:void 0,z=(E=Object.getPrototypeOf,_=Object,function(e){return E(_(e))}),V=Object.create,U=O.propertyIsEnumerable,q=C.splice,H=M?M.toStringTag:void 0,W=function(){try{var e=ye(Object,"defineProperty");return e({},"",{}),e}catch(t){}}(),J=F?F.isBuffer:void 0,G=Math.max,Y=Date.now,K=ye(y,"Map"),X=ye(Object,"create"),$=function(){function e(){}return function(t){if(!Pe(t))return{};if(V)return V(t);e.prototype=t;var n=new e;return e.prototype=void 0,n}}();function Q(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t-1},ee.prototype.set=function(e,t){var n=this.__data__,r=ae(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this},te.prototype.clear=function(){this.size=0,this.__data__={hash:new Q,map:new(K||ee),string:new Q}},te.prototype.delete=function(e){var t=ge(this,e).delete(e);return this.size-=t?1:0,t},te.prototype.get=function(e){return ge(this,e).get(e)},te.prototype.has=function(e){return ge(this,e).has(e)},te.prototype.set=function(e,t){var n=ge(this,e),r=n.size;return n.set(e,t),this.size+=n.size==r?0:1,this},ne.prototype.clear=function(){this.__data__=new ee,this.size=0},ne.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},ne.prototype.get=function(e){return this.__data__.get(e)},ne.prototype.has=function(e){return this.__data__.has(e)},ne.prototype.set=function(e,t){var n=this.__data__;if(n instanceof ee){var r=n.__data__;if(!K||r.length<199)return r.push([e,t]),this.size=++n.size,this;n=this.__data__=new te(r)}return n.set(e,t),this.size=n.size,this};var se,le=function(e,t,n){for(var r=-1,o=Object(e),i=n(e),a=i.length;a--;){var u=i[se?a:++r];if(!1===t(o[u],u,o))break}return e};function ce(e){return null==e?void 0===e?d:l:H&&H in Object(e)?function(e){var t=A.call(e,H),n=e[H];try{e[H]=void 0;var r=!0}catch(i){}var o=R.call(e);r&&(t?e[H]=n:delete e[H]);return o}(e):function(e){return R.call(e)}(e)}function fe(e){return Ne(e)&&ce(e)==i}function de(e){return!(!Pe(e)||function(e){return!!D&&D in e}(e))&&(Ie(e)?Z:p).test(function(e){if(null!=e){try{return N.call(e)}catch(t){}try{return e+""}catch(t){}}return""}(e))}function pe(e){if(!Pe(e))return function(e){var t=[];if(null!=e)for(var n in Object(e))t.push(n);return t}(e);var t=we(e),n=[];for(var r in e)("constructor"!=r||!t&&A.call(e,r))&&n.push(r);return n}function he(e,t,n,r,o){e!==t&&le(t,(function(i,a){if(o||(o=new ne),Pe(i))!function(e,t,n,r,o,i,a){var u=ke(e,n),s=ke(t,n),l=a.get(s);if(l)return void oe(e,n,l);var f=i?i(u,s,n+"",e,t,a):void 0,d=void 0===f;if(d){var p=Ee(s),h=!p&&Ce(s),v=!p&&!h&&Ae(s);f=s,p||h||v?Ee(u)?f=u:Ne(m=u)&&_e(m)?f=function(e,t){var n=-1,r=e.length;t||(t=Array(r));for(;++n-1&&e%1==0&&e0){if(++t>=800)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}(me);function Se(e,t){return e===t||e!==e&&t!==t}var Te=fe(function(){return arguments}())?fe:function(e){return Ne(e)&&A.call(e,"callee")&&!U.call(e,"callee")},Ee=Array.isArray;function _e(e){return null!=e&&Oe(e.length)&&!Ie(e)}var Ce=J||function(){return!1};function Ie(e){if(!Pe(e))return!1;var t=ce(e);return t==u||t==s||t==a||t==f}function Oe(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=o}function Pe(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function Ne(e){return null!=e&&"object"==typeof e}var Ae=T?function(e){return function(t){return e(t)}}(T):function(e){return Ne(e)&&Oe(e.length)&&!!v[ce(e)]};function De(e){return _e(e)?re(e,!0):pe(e)}var Re,je=(Re=function(e,t,n,r){he(e,t,n,r)},ve((function(e,t){var n=-1,r=t.length,o=r>1?t[r-1]:void 0,i=r>2?t[2]:void 0;for(o=Re.length>3&&"function"==typeof o?(r--,o):void 0,i&&function(e,t,n){if(!Pe(n))return!1;var r=typeof t;return!!("number"==r?_e(n)&&be(t,n.length):"string"==r&&t in n)&&Se(n[t],e)}(t[0],t[1],i)&&(o=r<3?void 0:o,r=1),e=Object(e);++n"']/g,K=RegExp(G.source),X=RegExp(Y.source),$=/<%-([\s\S]+?)%>/g,Q=/<%([\s\S]+?)%>/g,ee=/<%=([\s\S]+?)%>/g,te=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,ne=/^\w*$/,re=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,oe=/[\\^$.*+?()[\]{}|]/g,ie=RegExp(oe.source),ae=/^\s+/,ue=/\s/,se=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,le=/\{\n\/\* \[wrapped with (.+)\] \*/,ce=/,? & /,fe=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,de=/[()=,{}\[\]\/\s]/,pe=/\\(\\)?/g,he=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,ve=/\w*$/,me=/^[-+]0x[0-9a-f]+$/i,ge=/^0b[01]+$/i,ye=/^\[object .+?Constructor\]$/,be=/^0o[0-7]+$/i,we=/^(?:0|[1-9]\d*)$/,ke=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,xe=/($^)/,Se=/['\n\r\u2028\u2029\\]/g,Te="\\ud800-\\udfff",Ee="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",_e="\\u2700-\\u27bf",Ce="a-z\\xdf-\\xf6\\xf8-\\xff",Ie="A-Z\\xc0-\\xd6\\xd8-\\xde",Oe="\\ufe0e\\ufe0f",Pe="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Ne="['\u2019]",Ae="["+Te+"]",De="["+Pe+"]",Re="["+Ee+"]",je="\\d+",Ze="["+_e+"]",Fe="["+Ce+"]",Me="[^"+Te+Pe+je+_e+Ce+Ie+"]",Le="\\ud83c[\\udffb-\\udfff]",Be="[^"+Te+"]",ze="(?:\\ud83c[\\udde6-\\uddff]){2}",Ve="[\\ud800-\\udbff][\\udc00-\\udfff]",Ue="["+Ie+"]",qe="\\u200d",He="(?:"+Fe+"|"+Me+")",We="(?:"+Ue+"|"+Me+")",Je="(?:['\u2019](?:d|ll|m|re|s|t|ve))?",Ge="(?:['\u2019](?:D|LL|M|RE|S|T|VE))?",Ye="(?:"+Re+"|"+Le+")"+"?",Ke="["+Oe+"]?",Xe=Ke+Ye+("(?:"+qe+"(?:"+[Be,ze,Ve].join("|")+")"+Ke+Ye+")*"),$e="(?:"+[Ze,ze,Ve].join("|")+")"+Xe,Qe="(?:"+[Be+Re+"?",Re,ze,Ve,Ae].join("|")+")",et=RegExp(Ne,"g"),tt=RegExp(Re,"g"),nt=RegExp(Le+"(?="+Le+")|"+Qe+Xe,"g"),rt=RegExp([Ue+"?"+Fe+"+"+Je+"(?="+[De,Ue,"$"].join("|")+")",We+"+"+Ge+"(?="+[De,Ue+He,"$"].join("|")+")",Ue+"?"+He+"+"+Je,Ue+"+"+Ge,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",je,$e].join("|"),"g"),ot=RegExp("["+qe+Te+Ee+Oe+"]"),it=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,at=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],ut=-1,st={};st[Z]=st[F]=st[M]=st[L]=st[B]=st[z]=st[V]=st[U]=st[q]=!0,st[y]=st[b]=st[R]=st[w]=st[j]=st[k]=st[x]=st[S]=st[E]=st[_]=st[C]=st[O]=st[P]=st[N]=st[D]=!1;var lt={};lt[y]=lt[b]=lt[R]=lt[j]=lt[w]=lt[k]=lt[Z]=lt[F]=lt[M]=lt[L]=lt[B]=lt[E]=lt[_]=lt[C]=lt[O]=lt[P]=lt[N]=lt[A]=lt[z]=lt[V]=lt[U]=lt[q]=!0,lt[x]=lt[S]=lt[D]=!1;var ct={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},ft=parseFloat,dt=parseInt,pt="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,ht="object"==typeof self&&self&&self.Object===Object&&self,vt=pt||ht||Function("return this")(),mt=t&&!t.nodeType&&t,gt=mt&&e&&!e.nodeType&&e,yt=gt&>.exports===mt,bt=yt&&pt.process,wt=function(){try{var e=gt&>.require&>.require("util").types;return e||bt&&bt.binding&&bt.binding("util")}catch(t){}}(),kt=wt&&wt.isArrayBuffer,xt=wt&&wt.isDate,St=wt&&wt.isMap,Tt=wt&&wt.isRegExp,Et=wt&&wt.isSet,_t=wt&&wt.isTypedArray;function Ct(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}function It(e,t,n,r){for(var o=-1,i=null==e?0:e.length;++o-1}function Rt(e,t,n){for(var r=-1,o=null==e?0:e.length;++r-1;);return n}function rn(e,t){for(var n=e.length;n--&&Ut(t,e[n],0)>-1;);return n}var on=Gt({"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"}),an=Gt({"&":"&","<":"<",">":">",'"':""","'":"'"});function un(e){return"\\"+ct[e]}function sn(e){return ot.test(e)}function ln(e){var t=-1,n=Array(e.size);return e.forEach((function(e,r){n[++t]=[r,e]})),n}function cn(e,t){return function(n){return e(t(n))}}function fn(e,t){for(var n=-1,r=e.length,o=0,i=[];++n",""":'"',"'":"'"});var yn=function e(t){var n=(t=null==t?vt:yn.defaults(vt.Object(),t,yn.pick(vt,at))).Array,r=t.Date,ue=t.Error,Te=t.Function,Ee=t.Math,_e=t.Object,Ce=t.RegExp,Ie=t.String,Oe=t.TypeError,Pe=n.prototype,Ne=Te.prototype,Ae=_e.prototype,De=t["__core-js_shared__"],Re=Ne.toString,je=Ae.hasOwnProperty,Ze=0,Fe=function(){var e=/[^.]+$/.exec(De&&De.keys&&De.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""}(),Me=Ae.toString,Le=Re.call(_e),Be=vt._,ze=Ce("^"+Re.call(je).replace(oe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Ve=yt?t.Buffer:o,Ue=t.Symbol,qe=t.Uint8Array,He=Ve?Ve.allocUnsafe:o,We=cn(_e.getPrototypeOf,_e),Je=_e.create,Ge=Ae.propertyIsEnumerable,Ye=Pe.splice,Ke=Ue?Ue.isConcatSpreadable:o,Xe=Ue?Ue.iterator:o,$e=Ue?Ue.toStringTag:o,Qe=function(){try{var e=di(_e,"defineProperty");return e({},"",{}),e}catch(t){}}(),nt=t.clearTimeout!==vt.clearTimeout&&t.clearTimeout,ot=r&&r.now!==vt.Date.now&&r.now,ct=t.setTimeout!==vt.setTimeout&&t.setTimeout,pt=Ee.ceil,ht=Ee.floor,mt=_e.getOwnPropertySymbols,gt=Ve?Ve.isBuffer:o,bt=t.isFinite,wt=Pe.join,Bt=cn(_e.keys,_e),Gt=Ee.max,bn=Ee.min,wn=r.now,kn=t.parseInt,xn=Ee.random,Sn=Pe.reverse,Tn=di(t,"DataView"),En=di(t,"Map"),_n=di(t,"Promise"),Cn=di(t,"Set"),In=di(t,"WeakMap"),On=di(_e,"create"),Pn=In&&new In,Nn={},An=Mi(Tn),Dn=Mi(En),Rn=Mi(_n),jn=Mi(Cn),Zn=Mi(In),Fn=Ue?Ue.prototype:o,Mn=Fn?Fn.valueOf:o,Ln=Fn?Fn.toString:o;function Bn(e){if(tu(e)&&!qa(e)&&!(e instanceof qn)){if(e instanceof Un)return e;if(je.call(e,"__wrapped__"))return Li(e)}return new Un(e)}var zn=function(){function e(){}return function(t){if(!eu(t))return{};if(Je)return Je(t);e.prototype=t;var n=new e;return e.prototype=o,n}}();function Vn(){}function Un(e,t){this.__wrapped__=e,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=o}function qn(e){this.__wrapped__=e,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=m,this.__views__=[]}function Hn(e){var t=-1,n=null==e?0:e.length;for(this.clear();++t=t?e:t)),e}function sr(e,t,n,r,i,a){var u,s=1&t,l=2&t,c=4&t;if(n&&(u=i?n(e,r,i,a):n(e)),u!==o)return u;if(!eu(e))return e;var f=qa(e);if(f){if(u=function(e){var t=e.length,n=new e.constructor(t);t&&"string"==typeof e[0]&&je.call(e,"index")&&(n.index=e.index,n.input=e.input);return n}(e),!s)return Po(e,u)}else{var d=vi(e),p=d==S||d==T;if(Ga(e))return To(e,s);if(d==C||d==y||p&&!i){if(u=l||p?{}:gi(e),!s)return l?function(e,t){return No(e,hi(e),t)}(e,function(e,t){return e&&No(t,Au(t),e)}(u,e)):function(e,t){return No(e,pi(e),t)}(e,or(u,e))}else{if(!lt[d])return i?e:{};u=function(e,t,n){var r=e.constructor;switch(t){case R:return Eo(e);case w:case k:return new r(+e);case j:return function(e,t){var n=t?Eo(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.byteLength)}(e,n);case Z:case F:case M:case L:case B:case z:case V:case U:case q:return _o(e,n);case E:return new r;case _:case N:return new r(e);case O:return function(e){var t=new e.constructor(e.source,ve.exec(e));return t.lastIndex=e.lastIndex,t}(e);case P:return new r;case A:return o=e,Mn?_e(Mn.call(o)):{}}var o}(e,d,s)}}a||(a=new Yn);var h=a.get(e);if(h)return h;a.set(e,u),au(e)?e.forEach((function(r){u.add(sr(r,t,n,r,e,a))})):nu(e)&&e.forEach((function(r,o){u.set(o,sr(r,t,n,o,e,a))}));var v=f?o:(c?l?ii:oi:l?Au:Nu)(e);return Ot(v||e,(function(r,o){v&&(r=e[o=r]),tr(u,o,sr(r,t,n,o,e,a))})),u}function lr(e,t,n){var r=n.length;if(null==e)return!r;for(e=_e(e);r--;){var i=n[r],a=t[i],u=e[i];if(u===o&&!(i in e)||!a(u))return!1}return!0}function cr(e,t,n){if("function"!=typeof e)throw new Oe(i);return Ni((function(){e.apply(o,n)}),t)}function fr(e,t,n,r){var o=-1,i=Dt,a=!0,u=e.length,s=[],l=t.length;if(!u)return s;n&&(t=jt(t,Qt(n))),r?(i=Rt,a=!1):t.length>=200&&(i=tn,a=!1,t=new Gn(t));e:for(;++o-1},Wn.prototype.set=function(e,t){var n=this.__data__,r=nr(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this},Jn.prototype.clear=function(){this.size=0,this.__data__={hash:new Hn,map:new(En||Wn),string:new Hn}},Jn.prototype.delete=function(e){var t=ci(this,e).delete(e);return this.size-=t?1:0,t},Jn.prototype.get=function(e){return ci(this,e).get(e)},Jn.prototype.has=function(e){return ci(this,e).has(e)},Jn.prototype.set=function(e,t){var n=ci(this,e),r=n.size;return n.set(e,t),this.size+=n.size==r?0:1,this},Gn.prototype.add=Gn.prototype.push=function(e){return this.__data__.set(e,a),this},Gn.prototype.has=function(e){return this.__data__.has(e)},Yn.prototype.clear=function(){this.__data__=new Wn,this.size=0},Yn.prototype.delete=function(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n},Yn.prototype.get=function(e){return this.__data__.get(e)},Yn.prototype.has=function(e){return this.__data__.has(e)},Yn.prototype.set=function(e,t){var n=this.__data__;if(n instanceof Wn){var r=n.__data__;if(!En||r.length<199)return r.push([e,t]),this.size=++n.size,this;n=this.__data__=new Jn(r)}return n.set(e,t),this.size=n.size,this};var dr=Ro(wr),pr=Ro(kr,!0);function hr(e,t){var n=!0;return dr(e,(function(e,r,o){return n=!!t(e,r,o)})),n}function vr(e,t,n){for(var r=-1,i=e.length;++r0&&n(u)?t>1?gr(u,t-1,n,r,o):Zt(o,u):r||(o[o.length]=u)}return o}var yr=jo(),br=jo(!0);function wr(e,t){return e&&yr(e,t,Nu)}function kr(e,t){return e&&br(e,t,Nu)}function xr(e,t){return At(t,(function(t){return Xa(e[t])}))}function Sr(e,t){for(var n=0,r=(t=wo(t,e)).length;null!=e&&nt}function Cr(e,t){return null!=e&&je.call(e,t)}function Ir(e,t){return null!=e&&t in _e(e)}function Or(e,t,r){for(var i=r?Rt:Dt,a=e[0].length,u=e.length,s=u,l=n(u),c=1/0,f=[];s--;){var d=e[s];s&&t&&(d=jt(d,Qt(t))),c=bn(d.length,c),l[s]=!r&&(t||a>=120&&d.length>=120)?new Gn(s&&d):o}d=e[0];var p=-1,h=l[0];e:for(;++p=u?s:s*("desc"==n[r]?-1:1)}return e.index-t.index}(e,t,n)}))}function Hr(e,t,n){for(var r=-1,o=t.length,i={};++r-1;)u!==e&&Ye.call(u,s,1),Ye.call(e,s,1);return e}function Jr(e,t){for(var n=e?t.length:0,r=n-1;n--;){var o=t[n];if(n==r||o!==i){var i=o;bi(o)?Ye.call(e,o,1):fo(e,o)}}return e}function Gr(e,t){return e+ht(xn()*(t-e+1))}function Yr(e,t){var n="";if(!e||t<1||t>h)return n;do{t%2&&(n+=e),(t=ht(t/2))&&(e+=e)}while(t);return n}function Kr(e,t){return Ai(Ci(e,t,rs),e+"")}function Xr(e){return Xn(Bu(e))}function $r(e,t){var n=Bu(e);return ji(n,ur(t,0,n.length))}function Qr(e,t,n,r){if(!eu(e))return e;for(var i=-1,a=(t=wo(t,e)).length,u=a-1,s=e;null!=s&&++ii?0:i+t),(r=r>i?i:r)<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var a=n(i);++o>>1,a=e[i];null!==a&&!su(a)&&(n?a<=t:a=200){var l=t?null:Ko(e);if(l)return dn(l);a=!1,o=tn,s=new Gn}else s=t?[]:u;e:for(;++r=r?e:ro(e,t,n)}var So=nt||function(e){return vt.clearTimeout(e)};function To(e,t){if(t)return e.slice();var n=e.length,r=He?He(n):new e.constructor(n);return e.copy(r),r}function Eo(e){var t=new e.constructor(e.byteLength);return new qe(t).set(new qe(e)),t}function _o(e,t){var n=t?Eo(e.buffer):e.buffer;return new e.constructor(n,e.byteOffset,e.length)}function Co(e,t){if(e!==t){var n=e!==o,r=null===e,i=e===e,a=su(e),u=t!==o,s=null===t,l=t===t,c=su(t);if(!s&&!c&&!a&&e>t||a&&u&&l&&!s&&!c||r&&u&&l||!n&&l||!i)return 1;if(!r&&!a&&!c&&e1?n[i-1]:o,u=i>2?n[2]:o;for(a=e.length>3&&"function"==typeof a?(i--,a):o,u&&wi(n[0],n[1],u)&&(a=i<3?o:a,i=1),t=_e(t);++r-1?i[a?t[u]:u]:o}}function Bo(e){return ri((function(t){var n=t.length,r=n,a=Un.prototype.thru;for(e&&t.reverse();r--;){var u=t[r];if("function"!=typeof u)throw new Oe(i);if(a&&!s&&"wrapper"==ui(u))var s=new Un([],!0)}for(r=s?r:n;++r1&&w.reverse(),p&&cs))return!1;var c=a.get(e),f=a.get(t);if(c&&f)return c==t&&f==e;var d=-1,p=!0,h=2&n?new Gn:o;for(a.set(e,t),a.set(t,e);++d-1&&e%1==0&&e1?"& ":"")+t[r],t=t.join(n>2?", ":" "),e.replace(se,"{\n/* [wrapped with "+t+"] */\n")}(r,function(e,t){return Ot(g,(function(n){var r="_."+n[0];t&n[1]&&!Dt(e,r)&&e.push(r)})),e.sort()}(function(e){var t=e.match(le);return t?t[1].split(ce):[]}(r),n)))}function Ri(e){var t=0,n=0;return function(){var r=wn(),i=16-(r-n);if(n=r,i>0){if(++t>=800)return arguments[0]}else t=0;return e.apply(o,arguments)}}function ji(e,t){var n=-1,r=e.length,i=r-1;for(t=t===o?r:t;++n1?e[t-1]:o;return n="function"==typeof n?(e.pop(),n):o,ia(e,n)}));function da(e){var t=Bn(e);return t.__chain__=!0,t}function pa(e,t){return t(e)}var ha=ri((function(e){var t=e.length,n=t?e[0]:0,r=this.__wrapped__,i=function(t){return ar(t,e)};return!(t>1||this.__actions__.length)&&r instanceof qn&&bi(n)?((r=r.slice(n,+n+(t?1:0))).__actions__.push({func:pa,args:[i],thisArg:o}),new Un(r,this.__chain__).thru((function(e){return t&&!e.length&&e.push(o),e}))):this.thru(i)}));var va=Ao((function(e,t,n){je.call(e,n)?++e[n]:ir(e,n,1)}));var ma=Lo(Ui),ga=Lo(qi);function ya(e,t){return(qa(e)?Ot:dr)(e,li(t,3))}function ba(e,t){return(qa(e)?Pt:pr)(e,li(t,3))}var wa=Ao((function(e,t,n){je.call(e,n)?e[n].push(t):ir(e,n,[t])}));var ka=Kr((function(e,t,r){var o=-1,i="function"==typeof t,a=Wa(e)?n(e.length):[];return dr(e,(function(e){a[++o]=i?Ct(t,e,r):Pr(e,t,r)})),a})),xa=Ao((function(e,t,n){ir(e,n,t)}));function Sa(e,t){return(qa(e)?jt:Lr)(e,li(t,3))}var Ta=Ao((function(e,t,n){e[n?0:1].push(t)}),(function(){return[[],[]]}));var Ea=Kr((function(e,t){if(null==e)return[];var n=t.length;return n>1&&wi(e,t[0],t[1])?t=[]:n>2&&wi(t[0],t[1],t[2])&&(t=[t[0]]),qr(e,gr(t,1),[])})),_a=ot||function(){return vt.Date.now()};function Ca(e,t,n){return t=n?o:t,t=e&&null==t?e.length:t,$o(e,f,o,o,o,o,t)}function Ia(e,t){var n;if("function"!=typeof t)throw new Oe(i);return e=hu(e),function(){return--e>0&&(n=t.apply(this,arguments)),e<=1&&(t=o),n}}var Oa=Kr((function(e,t,n){var r=1;if(n.length){var o=fn(n,si(Oa));r|=l}return $o(e,r,t,n,o)})),Pa=Kr((function(e,t,n){var r=3;if(n.length){var o=fn(n,si(Pa));r|=l}return $o(t,r,e,n,o)}));function Na(e,t,n){var r,a,u,s,l,c,f=0,d=!1,p=!1,h=!0;if("function"!=typeof e)throw new Oe(i);function v(t){var n=r,i=a;return r=a=o,f=t,s=e.apply(i,n)}function m(e){var n=e-c;return c===o||n>=t||n<0||p&&e-f>=u}function g(){var e=_a();if(m(e))return y(e);l=Ni(g,function(e){var n=t-(e-c);return p?bn(n,u-(e-f)):n}(e))}function y(e){return l=o,h&&r?v(e):(r=a=o,s)}function b(){var e=_a(),n=m(e);if(r=arguments,a=this,c=e,n){if(l===o)return function(e){return f=e,l=Ni(g,t),d?v(e):s}(c);if(p)return So(l),l=Ni(g,t),v(c)}return l===o&&(l=Ni(g,t)),s}return t=mu(t)||0,eu(n)&&(d=!!n.leading,u=(p="maxWait"in n)?Gt(mu(n.maxWait)||0,t):u,h="trailing"in n?!!n.trailing:h),b.cancel=function(){l!==o&&So(l),f=0,r=c=a=l=o},b.flush=function(){return l===o?s:y(_a())},b}var Aa=Kr((function(e,t){return cr(e,1,t)})),Da=Kr((function(e,t,n){return cr(e,mu(t)||0,n)}));function Ra(e,t){if("function"!=typeof e||null!=t&&"function"!=typeof t)throw new Oe(i);var n=function n(){var r=arguments,o=t?t.apply(this,r):r[0],i=n.cache;if(i.has(o))return i.get(o);var a=e.apply(this,r);return n.cache=i.set(o,a)||i,a};return n.cache=new(Ra.Cache||Jn),n}function ja(e){if("function"!=typeof e)throw new Oe(i);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}Ra.Cache=Jn;var Za=ko((function(e,t){var n=(t=1==t.length&&qa(t[0])?jt(t[0],Qt(li())):jt(gr(t,1),Qt(li()))).length;return Kr((function(r){for(var o=-1,i=bn(r.length,n);++o=t})),Ua=Nr(function(){return arguments}())?Nr:function(e){return tu(e)&&je.call(e,"callee")&&!Ge.call(e,"callee")},qa=n.isArray,Ha=kt?Qt(kt):function(e){return tu(e)&&Er(e)==R};function Wa(e){return null!=e&&Qa(e.length)&&!Xa(e)}function Ja(e){return tu(e)&&Wa(e)}var Ga=gt||ms,Ya=xt?Qt(xt):function(e){return tu(e)&&Er(e)==k};function Ka(e){if(!tu(e))return!1;var t=Er(e);return t==x||"[object DOMException]"==t||"string"==typeof e.message&&"string"==typeof e.name&&!ou(e)}function Xa(e){if(!eu(e))return!1;var t=Er(e);return t==S||t==T||"[object AsyncFunction]"==t||"[object Proxy]"==t}function $a(e){return"number"==typeof e&&e==hu(e)}function Qa(e){return"number"==typeof e&&e>-1&&e%1==0&&e<=h}function eu(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}function tu(e){return null!=e&&"object"==typeof e}var nu=St?Qt(St):function(e){return tu(e)&&vi(e)==E};function ru(e){return"number"==typeof e||tu(e)&&Er(e)==_}function ou(e){if(!tu(e)||Er(e)!=C)return!1;var t=We(e);if(null===t)return!0;var n=je.call(t,"constructor")&&t.constructor;return"function"==typeof n&&n instanceof n&&Re.call(n)==Le}var iu=Tt?Qt(Tt):function(e){return tu(e)&&Er(e)==O};var au=Et?Qt(Et):function(e){return tu(e)&&vi(e)==P};function uu(e){return"string"==typeof e||!qa(e)&&tu(e)&&Er(e)==N}function su(e){return"symbol"==typeof e||tu(e)&&Er(e)==A}var lu=_t?Qt(_t):function(e){return tu(e)&&Qa(e.length)&&!!st[Er(e)]};var cu=Jo(Mr),fu=Jo((function(e,t){return e<=t}));function du(e){if(!e)return[];if(Wa(e))return uu(e)?vn(e):Po(e);if(Xe&&e[Xe])return function(e){for(var t,n=[];!(t=e.next()).done;)n.push(t.value);return n}(e[Xe]());var t=vi(e);return(t==E?ln:t==P?dn:Bu)(e)}function pu(e){return e?(e=mu(e))===p||e===-1/0?17976931348623157e292*(e<0?-1:1):e===e?e:0:0===e?e:0}function hu(e){var t=pu(e),n=t%1;return t===t?n?t-n:t:0}function vu(e){return e?ur(hu(e),0,m):0}function mu(e){if("number"==typeof e)return e;if(su(e))return v;if(eu(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=eu(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=$t(e);var n=ge.test(e);return n||be.test(e)?dt(e.slice(2),n?2:8):me.test(e)?v:+e}function gu(e){return No(e,Au(e))}function yu(e){return null==e?"":lo(e)}var bu=Do((function(e,t){if(Ti(t)||Wa(t))No(t,Nu(t),e);else for(var n in t)je.call(t,n)&&tr(e,n,t[n])})),wu=Do((function(e,t){No(t,Au(t),e)})),ku=Do((function(e,t,n,r){No(t,Au(t),e,r)})),xu=Do((function(e,t,n,r){No(t,Nu(t),e,r)})),Su=ri(ar);var Tu=Kr((function(e,t){e=_e(e);var n=-1,r=t.length,i=r>2?t[2]:o;for(i&&wi(t[0],t[1],i)&&(r=1);++n1),t})),No(e,ii(e),n),r&&(n=sr(n,7,ti));for(var o=t.length;o--;)fo(n,t[o]);return n}));var Zu=ri((function(e,t){return null==e?{}:function(e,t){return Hr(e,t,(function(t,n){return Cu(e,n)}))}(e,t)}));function Fu(e,t){if(null==e)return{};var n=jt(ii(e),(function(e){return[e]}));return t=li(t),Hr(e,n,(function(e,n){return t(e,n[0])}))}var Mu=Xo(Nu),Lu=Xo(Au);function Bu(e){return null==e?[]:en(e,Nu(e))}var zu=Fo((function(e,t,n){return t=t.toLowerCase(),e+(n?Vu(t):t)}));function Vu(e){return Ku(yu(e).toLowerCase())}function Uu(e){return(e=yu(e))&&e.replace(ke,on).replace(tt,"")}var qu=Fo((function(e,t,n){return e+(n?"-":"")+t.toLowerCase()})),Hu=Fo((function(e,t,n){return e+(n?" ":"")+t.toLowerCase()})),Wu=Zo("toLowerCase");var Ju=Fo((function(e,t,n){return e+(n?"_":"")+t.toLowerCase()}));var Gu=Fo((function(e,t,n){return e+(n?" ":"")+Ku(t)}));var Yu=Fo((function(e,t,n){return e+(n?" ":"")+t.toUpperCase()})),Ku=Zo("toUpperCase");function Xu(e,t,n){return e=yu(e),(t=n?o:t)===o?function(e){return it.test(e)}(e)?function(e){return e.match(rt)||[]}(e):function(e){return e.match(fe)||[]}(e):e.match(t)||[]}var $u=Kr((function(e,t){try{return Ct(e,o,t)}catch(n){return Ka(n)?n:new ue(n)}})),Qu=ri((function(e,t){return Ot(t,(function(t){t=Fi(t),ir(e,t,Oa(e[t],e))})),e}));function es(e){return function(){return e}}var ts=Bo(),ns=Bo(!0);function rs(e){return e}function os(e){return jr("function"==typeof e?e:sr(e,1))}var is=Kr((function(e,t){return function(n){return Pr(n,e,t)}})),as=Kr((function(e,t){return function(n){return Pr(e,n,t)}}));function us(e,t,n){var r=Nu(t),o=xr(t,r);null!=n||eu(t)&&(o.length||!r.length)||(n=t,t=e,e=this,o=xr(t,Nu(t)));var i=!(eu(n)&&"chain"in n)||!!n.chain,a=Xa(e);return Ot(o,(function(n){var r=t[n];e[n]=r,a&&(e.prototype[n]=function(){var t=this.__chain__;if(i||t){var n=e(this.__wrapped__);return(n.__actions__=Po(this.__actions__)).push({func:r,args:arguments,thisArg:e}),n.__chain__=t,n}return r.apply(e,Zt([this.value()],arguments))})})),e}function ss(){}var ls=qo(jt),cs=qo(Nt),fs=qo(Lt);function ds(e){return ki(e)?Jt(Fi(e)):function(e){return function(t){return Sr(t,e)}}(e)}var ps=Wo(),hs=Wo(!0);function vs(){return[]}function ms(){return!1}var gs=Uo((function(e,t){return e+t}),0),ys=Yo("ceil"),bs=Uo((function(e,t){return e/t}),1),ws=Yo("floor");var ks=Uo((function(e,t){return e*t}),1),xs=Yo("round"),Ss=Uo((function(e,t){return e-t}),0);return Bn.after=function(e,t){if("function"!=typeof t)throw new Oe(i);return e=hu(e),function(){if(--e<1)return t.apply(this,arguments)}},Bn.ary=Ca,Bn.assign=bu,Bn.assignIn=wu,Bn.assignInWith=ku,Bn.assignWith=xu,Bn.at=Su,Bn.before=Ia,Bn.bind=Oa,Bn.bindAll=Qu,Bn.bindKey=Pa,Bn.castArray=function(){if(!arguments.length)return[];var e=arguments[0];return qa(e)?e:[e]},Bn.chain=da,Bn.chunk=function(e,t,r){t=(r?wi(e,t,r):t===o)?1:Gt(hu(t),0);var i=null==e?0:e.length;if(!i||t<1)return[];for(var a=0,u=0,s=n(pt(i/t));ai?0:i+n),(r=r===o||r>i?i:hu(r))<0&&(r+=i),r=n>r?0:vu(r);n>>0)?(e=yu(e))&&("string"==typeof t||null!=t&&!iu(t))&&!(t=lo(t))&&sn(e)?xo(vn(e),0,n):e.split(t,n):[]},Bn.spread=function(e,t){if("function"!=typeof e)throw new Oe(i);return t=null==t?0:Gt(hu(t),0),Kr((function(n){var r=n[t],o=xo(n,0,t);return r&&Zt(o,r),Ct(e,this,o)}))},Bn.tail=function(e){var t=null==e?0:e.length;return t?ro(e,1,t):[]},Bn.take=function(e,t,n){return e&&e.length?ro(e,0,(t=n||t===o?1:hu(t))<0?0:t):[]},Bn.takeRight=function(e,t,n){var r=null==e?0:e.length;return r?ro(e,(t=r-(t=n||t===o?1:hu(t)))<0?0:t,r):[]},Bn.takeRightWhile=function(e,t){return e&&e.length?ho(e,li(t,3),!1,!0):[]},Bn.takeWhile=function(e,t){return e&&e.length?ho(e,li(t,3)):[]},Bn.tap=function(e,t){return t(e),e},Bn.throttle=function(e,t,n){var r=!0,o=!0;if("function"!=typeof e)throw new Oe(i);return eu(n)&&(r="leading"in n?!!n.leading:r,o="trailing"in n?!!n.trailing:o),Na(e,t,{leading:r,maxWait:t,trailing:o})},Bn.thru=pa,Bn.toArray=du,Bn.toPairs=Mu,Bn.toPairsIn=Lu,Bn.toPath=function(e){return qa(e)?jt(e,Fi):su(e)?[e]:Po(Zi(yu(e)))},Bn.toPlainObject=gu,Bn.transform=function(e,t,n){var r=qa(e),o=r||Ga(e)||lu(e);if(t=li(t,4),null==n){var i=e&&e.constructor;n=o?r?new i:[]:eu(e)&&Xa(i)?zn(We(e)):{}}return(o?Ot:wr)(e,(function(e,r,o){return t(n,e,r,o)})),n},Bn.unary=function(e){return Ca(e,1)},Bn.union=ta,Bn.unionBy=na,Bn.unionWith=ra,Bn.uniq=function(e){return e&&e.length?co(e):[]},Bn.uniqBy=function(e,t){return e&&e.length?co(e,li(t,2)):[]},Bn.uniqWith=function(e,t){return t="function"==typeof t?t:o,e&&e.length?co(e,o,t):[]},Bn.unset=function(e,t){return null==e||fo(e,t)},Bn.unzip=oa,Bn.unzipWith=ia,Bn.update=function(e,t,n){return null==e?e:po(e,t,bo(n))},Bn.updateWith=function(e,t,n,r){return r="function"==typeof r?r:o,null==e?e:po(e,t,bo(n),r)},Bn.values=Bu,Bn.valuesIn=function(e){return null==e?[]:en(e,Au(e))},Bn.without=aa,Bn.words=Xu,Bn.wrap=function(e,t){return Fa(bo(t),e)},Bn.xor=ua,Bn.xorBy=sa,Bn.xorWith=la,Bn.zip=ca,Bn.zipObject=function(e,t){return go(e||[],t||[],tr)},Bn.zipObjectDeep=function(e,t){return go(e||[],t||[],Qr)},Bn.zipWith=fa,Bn.entries=Mu,Bn.entriesIn=Lu,Bn.extend=wu,Bn.extendWith=ku,us(Bn,Bn),Bn.add=gs,Bn.attempt=$u,Bn.camelCase=zu,Bn.capitalize=Vu,Bn.ceil=ys,Bn.clamp=function(e,t,n){return n===o&&(n=t,t=o),n!==o&&(n=(n=mu(n))===n?n:0),t!==o&&(t=(t=mu(t))===t?t:0),ur(mu(e),t,n)},Bn.clone=function(e){return sr(e,4)},Bn.cloneDeep=function(e){return sr(e,5)},Bn.cloneDeepWith=function(e,t){return sr(e,5,t="function"==typeof t?t:o)},Bn.cloneWith=function(e,t){return sr(e,4,t="function"==typeof t?t:o)},Bn.conformsTo=function(e,t){return null==t||lr(e,t,Nu(t))},Bn.deburr=Uu,Bn.defaultTo=function(e,t){return null==e||e!==e?t:e},Bn.divide=bs,Bn.endsWith=function(e,t,n){e=yu(e),t=lo(t);var r=e.length,i=n=n===o?r:ur(hu(n),0,r);return(n-=t.length)>=0&&e.slice(n,i)==t},Bn.eq=Ba,Bn.escape=function(e){return(e=yu(e))&&X.test(e)?e.replace(Y,an):e},Bn.escapeRegExp=function(e){return(e=yu(e))&&ie.test(e)?e.replace(oe,"\\$&"):e},Bn.every=function(e,t,n){var r=qa(e)?Nt:hr;return n&&wi(e,t,n)&&(t=o),r(e,li(t,3))},Bn.find=ma,Bn.findIndex=Ui,Bn.findKey=function(e,t){return zt(e,li(t,3),wr)},Bn.findLast=ga,Bn.findLastIndex=qi,Bn.findLastKey=function(e,t){return zt(e,li(t,3),kr)},Bn.floor=ws,Bn.forEach=ya,Bn.forEachRight=ba,Bn.forIn=function(e,t){return null==e?e:yr(e,li(t,3),Au)},Bn.forInRight=function(e,t){return null==e?e:br(e,li(t,3),Au)},Bn.forOwn=function(e,t){return e&&wr(e,li(t,3))},Bn.forOwnRight=function(e,t){return e&&kr(e,li(t,3))},Bn.get=_u,Bn.gt=za,Bn.gte=Va,Bn.has=function(e,t){return null!=e&&mi(e,t,Cr)},Bn.hasIn=Cu,Bn.head=Wi,Bn.identity=rs,Bn.includes=function(e,t,n,r){e=Wa(e)?e:Bu(e),n=n&&!r?hu(n):0;var o=e.length;return n<0&&(n=Gt(o+n,0)),uu(e)?n<=o&&e.indexOf(t,n)>-1:!!o&&Ut(e,t,n)>-1},Bn.indexOf=function(e,t,n){var r=null==e?0:e.length;if(!r)return-1;var o=null==n?0:hu(n);return o<0&&(o=Gt(r+o,0)),Ut(e,t,o)},Bn.inRange=function(e,t,n){return t=pu(t),n===o?(n=t,t=0):n=pu(n),function(e,t,n){return e>=bn(t,n)&&e=-9007199254740991&&e<=h},Bn.isSet=au,Bn.isString=uu,Bn.isSymbol=su,Bn.isTypedArray=lu,Bn.isUndefined=function(e){return e===o},Bn.isWeakMap=function(e){return tu(e)&&vi(e)==D},Bn.isWeakSet=function(e){return tu(e)&&"[object WeakSet]"==Er(e)},Bn.join=function(e,t){return null==e?"":wt.call(e,t)},Bn.kebabCase=qu,Bn.last=Ki,Bn.lastIndexOf=function(e,t,n){var r=null==e?0:e.length;if(!r)return-1;var i=r;return n!==o&&(i=(i=hu(n))<0?Gt(r+i,0):bn(i,r-1)),t===t?function(e,t,n){for(var r=n+1;r--;)if(e[r]===t)return r;return r}(e,t,i):Vt(e,Ht,i,!0)},Bn.lowerCase=Hu,Bn.lowerFirst=Wu,Bn.lt=cu,Bn.lte=fu,Bn.max=function(e){return e&&e.length?vr(e,rs,_r):o},Bn.maxBy=function(e,t){return e&&e.length?vr(e,li(t,2),_r):o},Bn.mean=function(e){return Wt(e,rs)},Bn.meanBy=function(e,t){return Wt(e,li(t,2))},Bn.min=function(e){return e&&e.length?vr(e,rs,Mr):o},Bn.minBy=function(e,t){return e&&e.length?vr(e,li(t,2),Mr):o},Bn.stubArray=vs,Bn.stubFalse=ms,Bn.stubObject=function(){return{}},Bn.stubString=function(){return""},Bn.stubTrue=function(){return!0},Bn.multiply=ks,Bn.nth=function(e,t){return e&&e.length?Ur(e,hu(t)):o},Bn.noConflict=function(){return vt._===this&&(vt._=Be),this},Bn.noop=ss,Bn.now=_a,Bn.pad=function(e,t,n){e=yu(e);var r=(t=hu(t))?hn(e):0;if(!t||r>=t)return e;var o=(t-r)/2;return Ho(ht(o),n)+e+Ho(pt(o),n)},Bn.padEnd=function(e,t,n){e=yu(e);var r=(t=hu(t))?hn(e):0;return t&&rt){var r=e;e=t,t=r}if(n||e%1||t%1){var i=xn();return bn(e+i*(t-e+ft("1e-"+((i+"").length-1))),t)}return Gr(e,t)},Bn.reduce=function(e,t,n){var r=qa(e)?Ft:Yt,o=arguments.length<3;return r(e,li(t,4),n,o,dr)},Bn.reduceRight=function(e,t,n){var r=qa(e)?Mt:Yt,o=arguments.length<3;return r(e,li(t,4),n,o,pr)},Bn.repeat=function(e,t,n){return t=(n?wi(e,t,n):t===o)?1:hu(t),Yr(yu(e),t)},Bn.replace=function(){var e=arguments,t=yu(e[0]);return e.length<3?t:t.replace(e[1],e[2])},Bn.result=function(e,t,n){var r=-1,i=(t=wo(t,e)).length;for(i||(i=1,e=o);++rh)return[];var n=m,r=bn(e,m);t=li(t),e-=m;for(var o=Xt(r,t);++n=a)return e;var s=n-hn(r);if(s<1)return r;var l=u?xo(u,0,s).join(""):e.slice(0,s);if(i===o)return l+r;if(u&&(s+=l.length-s),iu(i)){if(e.slice(s).search(i)){var c,f=l;for(i.global||(i=Ce(i.source,yu(ve.exec(i))+"g")),i.lastIndex=0;c=i.exec(f);)var d=c.index;l=l.slice(0,d===o?s:d)}}else if(e.indexOf(lo(i),s)!=s){var p=l.lastIndexOf(i);p>-1&&(l=l.slice(0,p))}return l+r},Bn.unescape=function(e){return(e=yu(e))&&K.test(e)?e.replace(G,gn):e},Bn.uniqueId=function(e){var t=++Ze;return yu(e)+t},Bn.upperCase=Yu,Bn.upperFirst=Ku,Bn.each=ya,Bn.eachRight=ba,Bn.first=Wi,us(Bn,function(){var e={};return wr(Bn,(function(t,n){je.call(Bn.prototype,n)||(e[n]=t)})),e}(),{chain:!1}),Bn.VERSION="4.17.21",Ot(["bind","bindKey","curry","curryRight","partial","partialRight"],(function(e){Bn[e].placeholder=Bn})),Ot(["drop","take"],(function(e,t){qn.prototype[e]=function(n){n=n===o?1:Gt(hu(n),0);var r=this.__filtered__&&!t?new qn(this):this.clone();return r.__filtered__?r.__takeCount__=bn(n,r.__takeCount__):r.__views__.push({size:bn(n,m),type:e+(r.__dir__<0?"Right":"")}),r},qn.prototype[e+"Right"]=function(t){return this.reverse()[e](t).reverse()}})),Ot(["filter","map","takeWhile"],(function(e,t){var n=t+1,r=1==n||3==n;qn.prototype[e]=function(e){var t=this.clone();return t.__iteratees__.push({iteratee:li(e,3),type:n}),t.__filtered__=t.__filtered__||r,t}})),Ot(["head","last"],(function(e,t){var n="take"+(t?"Right":"");qn.prototype[e]=function(){return this[n](1).value()[0]}})),Ot(["initial","tail"],(function(e,t){var n="drop"+(t?"":"Right");qn.prototype[e]=function(){return this.__filtered__?new qn(this):this[n](1)}})),qn.prototype.compact=function(){return this.filter(rs)},qn.prototype.find=function(e){return this.filter(e).head()},qn.prototype.findLast=function(e){return this.reverse().find(e)},qn.prototype.invokeMap=Kr((function(e,t){return"function"==typeof e?new qn(this):this.map((function(n){return Pr(n,e,t)}))})),qn.prototype.reject=function(e){return this.filter(ja(li(e)))},qn.prototype.slice=function(e,t){e=hu(e);var n=this;return n.__filtered__&&(e>0||t<0)?new qn(n):(e<0?n=n.takeRight(-e):e&&(n=n.drop(e)),t!==o&&(n=(t=hu(t))<0?n.dropRight(-t):n.take(t-e)),n)},qn.prototype.takeRightWhile=function(e){return this.reverse().takeWhile(e).reverse()},qn.prototype.toArray=function(){return this.take(m)},wr(qn.prototype,(function(e,t){var n=/^(?:filter|find|map|reject)|While$/.test(t),r=/^(?:head|last)$/.test(t),i=Bn[r?"take"+("last"==t?"Right":""):t],a=r||/^find/.test(t);i&&(Bn.prototype[t]=function(){var t=this.__wrapped__,u=r?[1]:arguments,s=t instanceof qn,l=u[0],c=s||qa(t),f=function(e){var t=i.apply(Bn,Zt([e],u));return r&&d?t[0]:t};c&&n&&"function"==typeof l&&1!=l.length&&(s=c=!1);var d=this.__chain__,p=!!this.__actions__.length,h=a&&!d,v=s&&!p;if(!a&&c){t=v?t:new qn(this);var m=e.apply(t,u);return m.__actions__.push({func:pa,args:[f],thisArg:o}),new Un(m,d)}return h&&v?e.apply(this,u):(m=this.thru(f),h?r?m.value()[0]:m.value():m)})})),Ot(["pop","push","shift","sort","splice","unshift"],(function(e){var t=Pe[e],n=/^(?:push|sort|unshift)$/.test(e)?"tap":"thru",r=/^(?:pop|shift)$/.test(e);Bn.prototype[e]=function(){var e=arguments;if(r&&!this.__chain__){var o=this.value();return t.apply(qa(o)?o:[],e)}return this[n]((function(n){return t.apply(qa(n)?n:[],e)}))}})),wr(qn.prototype,(function(e,t){var n=Bn[t];if(n){var r=n.name+"";je.call(Nn,r)||(Nn[r]=[]),Nn[r].push({name:t,func:n})}})),Nn[zo(o,2).name]=[{name:"wrapper",func:o}],qn.prototype.clone=function(){var e=new qn(this.__wrapped__);return e.__actions__=Po(this.__actions__),e.__dir__=this.__dir__,e.__filtered__=this.__filtered__,e.__iteratees__=Po(this.__iteratees__),e.__takeCount__=this.__takeCount__,e.__views__=Po(this.__views__),e},qn.prototype.reverse=function(){if(this.__filtered__){var e=new qn(this);e.__dir__=-1,e.__filtered__=!0}else(e=this.clone()).__dir__*=-1;return e},qn.prototype.value=function(){var e=this.__wrapped__.value(),t=this.__dir__,n=qa(e),r=t<0,o=n?e.length:0,i=function(e,t,n){var r=-1,o=n.length;for(;++r=this.__values__.length;return{done:e,value:e?o:this.__values__[this.__index__++]}},Bn.prototype.plant=function(e){for(var t,n=this;n instanceof Vn;){var r=Li(n);r.__index__=0,r.__values__=o,t?i.__wrapped__=r:t=r;var i=r;n=n.__wrapped__}return i.__wrapped__=e,t},Bn.prototype.reverse=function(){var e=this.__wrapped__;if(e instanceof qn){var t=e;return this.__actions__.length&&(t=new qn(this)),(t=t.reverse()).__actions__.push({func:pa,args:[ea],thisArg:o}),new Un(t,this.__chain__)}return this.thru(ea)},Bn.prototype.toJSON=Bn.prototype.valueOf=Bn.prototype.value=function(){return vo(this.__wrapped__,this.__actions__)},Bn.prototype.first=Bn.prototype.head,Xe&&(Bn.prototype[Xe]=function(){return this}),Bn}();vt._=yn,(r=function(){return yn}.call(t,n,t,e))===o||(e.exports=r)}.call(this)},4463:function(e,t,n){"use strict";var r=n(2791),o=n(5296);function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n