diff --git a/www/src/components/Table/TableHeader/Sparks.tsx b/www/src/components/Table/TableHeader/Sparks.tsx index 335e4bbd..7bbfedac 100644 --- a/www/src/components/Table/TableHeader/Sparks.tsx +++ b/www/src/components/Table/TableHeader/Sparks.tsx @@ -17,7 +17,7 @@ import { useAppContext } from "contexts/AppContext"; import CodeEditor from "../editors/CodeEditor"; -export default function SparksEditor() { +export default function SparksEditor({ requestSnackLog }) { const snack = useSnackContext(); const { tableState, tableActions } = useFiretableContext(); const appContext = useAppContext(); @@ -64,6 +64,7 @@ export default function SparksEditor() { const userTokenInfo = await appContext?.currentUser?.getIdTokenResult(); const userToken = userTokenInfo?.token; try { + requestSnackLog(Date.now()); const response = await fetch(ftBuildUrl, { method: "POST", headers: { diff --git a/www/src/components/Table/TableHeader/TableLogs.tsx b/www/src/components/Table/TableHeader/TableLogs.tsx index 33f7f2e4..090f0cb9 100644 --- a/www/src/components/Table/TableHeader/TableLogs.tsx +++ b/www/src/components/Table/TableHeader/TableLogs.tsx @@ -18,12 +18,17 @@ import { Box, Tabs, Tab, + IconButton, } from "@material-ui/core"; import Modal from "components/Modal"; import { makeStyles } from "@material-ui/core/styles"; import LogsIcon from "@material-ui/icons/QueryBuilder"; import SuccessIcon from "@material-ui/icons/CheckCircle"; import FailIcon from "@material-ui/icons/Cancel"; +import ExpandIcon from "@material-ui/icons/ExpandLess"; +import CollapseIcon from "@material-ui/icons/ExpandMore"; +import OpenIcon from "@material-ui/icons/OpenInNew"; +import CloseIcon from "@material-ui/icons/Close"; import TableHeaderButton from "./TableHeaderButton"; import { LOG_FONT, LOG_TEXT } from "Themes"; import Ansi from "ansi-to-react"; @@ -38,6 +43,12 @@ function a11yProps(index) { }; } +const isTargetInsideBox = (target, box) => { + const targetRect = target.getBoundingClientRect(); + const boxRect = box.getBoundingClientRect(); + return targetRect.y < boxRect.y + boxRect.height; +}; + const useStyles = makeStyles((theme) => ({ root: { flexGrow: 1, @@ -60,7 +71,7 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: "#1E1E1E", }, logPanelProgress: { - marginLeft: "3em", + marginLeft: "2em", marginTop: "1em", }, logEntryWrapper: { @@ -69,13 +80,13 @@ const useStyles = makeStyles((theme) => ({ }, logNumber: { float: "left", - width: "3em", + width: "2em", textAlign: "right", paddingRight: "1em", }, logEntry: { lineBreak: "anywhere", - paddingLeft: "3em", + paddingLeft: "2em", whiteSpace: "break-spaces", userSelect: "text", }, @@ -96,6 +107,22 @@ const useStyles = makeStyles((theme) => ({ fontFamily: LOG_FONT, }, }, + + snackLog: { + position: "absolute", + left: 40, + bottom: 40, + backgroundColor: "#282829", + width: "min(40vw, 640px)", + padding: theme.spacing(1, 2, 2, 2), + borderRadius: 4, + zIndex: 1, + height: 300, + transition: "height 300ms ease-out", + }, + snackLogExpanded: { + height: "calc(100% - 300px)", + }, })); LogPanel.propTypes = { @@ -161,7 +188,7 @@ function LogPanel(props) { console.log("live streaming:", liveStreamTargetVisible); setLiveStreaming(liveStreamTargetVisible); } - }, 100); + }, 500); const scrollToLive = () => { const liveStreamTarget = document.querySelector("#live-stream-target"); @@ -170,12 +197,6 @@ function LogPanel(props) { }); }; - const isTargetInsideBox = (target, box) => { - const targetRect = target.getBoundingClientRect(); - const boxRect = box.getBoundingClientRect(); - return targetRect.y < boxRect.y + boxRect.height; - }; - useEffect(() => { if (liveStreaming && isActive && status === "BUILDING") { if (!liveStreamingRef.current) { @@ -230,13 +251,141 @@ function LogPanel(props) { ); } -export default function TableLogs() { +function SnackLog({ log, onClose, onOpenPanel }) { + const logs = log?.fullLog; + const status = log?.status; + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef( + true + ); + const liveStreamingRef = useRef(); + + const handleScroll = _throttle(() => { + const target = document.querySelector("#live-stream-target-snack"); + const scrollBox = document.querySelector("#live-stream-scroll-box-snack"); + const liveStreamTargetVisible = + target && scrollBox && isTargetInsideBox(target, scrollBox); + if (liveStreamTargetVisible !== liveStreamingStateRef.current) { + console.log("live streaming:", liveStreamTargetVisible); + setLiveStreaming(liveStreamTargetVisible); + } + }, 100); + + const scrollToLive = () => { + const liveStreamTarget = document.querySelector( + "#live-stream-target-snack" + ); + liveStreamTarget?.scrollIntoView?.(); + }; + + useEffect(() => { + if (liveStreaming && status === "BUILDING") { + if (!liveStreamingRef.current) { + scrollToLive(); + } else { + setTimeout(scrollToLive, 500); + } + } + }, [log]); + + useEffect(() => { + const liveStreamScrollBox = document.querySelector( + "#live-stream-scroll-box-snack" + ); + liveStreamScrollBox!.addEventListener("scroll", () => { + handleScroll(); + }); + }, []); + + return ( + + + + {!log && Build Pending...} + {log?.status === "SUCCESS" && ( + + Build Completed + + )} + {log?.status === "FAIL" && ( + + Build Failed + + )} + {log?.status === "BUILDING" && Building...} + + + setExpanded(!expanded)} + > + {expanded ? : } + + + + + + + + + + + + {log && ( + <> + {logs?.map((log, index) => { + return ; + })} +
+ {status === "BUILDING" && ( + + )} +
+
+ + )} + + + ); +} + +export default function TableLogs({ requestSnackLog }) { const router = useRouter(); const { tableState } = useFiretableContext(); const classes = useStyles(); - const [open, setOpen] = useState(false); + const [panalOpen, setPanelOpen] = useState(false); + const [snackOpen, setSnackOpen] = useState(false); const [tabIndex, setTabIndex] = React.useState(0); + const [activeLogTimestamp, setActiveLogTimestamp] = useState(Date.now()); + + useEffect(() => { + if (requestSnackLog > 0) { + setTimeout(() => { + setActiveLogTimestamp(requestSnackLog); + setSnackOpen(true); + }, 500); + } + }, [requestSnackLog]); const tableCollection = decodeURIComponent(router.match.params.id); const ftBuildStreamID = @@ -253,21 +402,20 @@ export default function TableLogs() { path: `${ftBuildStreamID}/ftBuildLogs`, orderBy: [{ key: "startTimeStamp", direction: "desc" }], }); - const latestStatus = collectionState?.rows?.[0]?.status; + const latestLog = collectionState?.rows?.[0]; + const latestStatus = latestLog?.status; + const latestActiveLog = + latestLog?.startTimeStamp > activeLogTimestamp ? latestLog : null; const handleTabChange = (event, newValue) => { setTabIndex(newValue); }; - const handleClose = () => { - setOpen(false); - }; - return ( <> setOpen(true)} + onClick={() => setPanelOpen(true)} icon={ <> {latestStatus === "BUILDING" && } @@ -278,9 +426,21 @@ export default function TableLogs() { } /> - {open && !!tableState && ( + {snackOpen && ( + setSnackOpen(false)} + onOpenPanel={() => { + setPanelOpen(true); + }} + /> + )} + + {panalOpen && !!tableState && ( { + setPanelOpen(false); + }} maxWidth="xl" fullWidth title={ diff --git a/www/src/components/Table/TableHeader/index.tsx b/www/src/components/Table/TableHeader/index.tsx index 9331e0d5..514037f4 100644 --- a/www/src/components/Table/TableHeader/index.tsx +++ b/www/src/components/Table/TableHeader/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { makeStyles, @@ -86,6 +86,11 @@ export default function TableHeader({ const { currentUser } = useAppContext(); const { tableActions, tableState, userClaims } = useFiretableContext(); + const [snackCount, setSnackCount] = useState(0); + + const openSnackLog = (requestTimestamp) => { + setSnackCount(requestTimestamp); + }; const hasDerivatives = tableState && @@ -210,7 +215,13 @@ export default function TableHeader({ {userClaims?.roles?.includes("ADMIN") && ( - + + + )} + + {userClaims?.roles?.includes("ADMIN") && ( + + )} @@ -220,10 +231,6 @@ export default function TableHeader({ )} - - - -