diff --git a/www/package.json b/www/package.json index 8d616972..82248a27 100644 --- a/www/package.json +++ b/www/package.json @@ -49,6 +49,7 @@ "react-router-dom": "^5.0.1", "react-scripts": "^3.4.3", "react-scroll-sync": "^0.8.0", + "react-usestateref": "^1.0.5", "serve": "^11.3.2", "tinymce": "^5.2.0", "typescript": "^3.7.2", 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 2e181fb6..8320961f 100644 --- a/www/src/components/Table/TableHeader/TableLogs.tsx +++ b/www/src/components/Table/TableHeader/TableLogs.tsx @@ -1,12 +1,15 @@ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import useRouter from "hooks/useRouter"; import useTable from "hooks/useFiretable/useTable"; import { useFiretableContext } from "contexts/FiretableContext"; +import useStateRef from "react-usestateref"; +import { db } from "../../../firebase"; import _camelCase from "lodash/camelCase"; import _get from "lodash/get"; import _find from "lodash/find"; import _sortBy from "lodash/sortBy"; +import _throttle from "lodash/throttle"; import moment from "moment"; import { @@ -16,12 +19,18 @@ import { Box, Tabs, Tab, + IconButton, + Link, } 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"; @@ -36,6 +45,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, @@ -57,19 +72,23 @@ const useStyles = makeStyles((theme) => ({ width: "100%", backgroundColor: "#1E1E1E", }, + logPanelProgress: { + marginLeft: "2em", + marginTop: "1em", + }, logEntryWrapper: { overflowY: "scroll", maxHeight: "100%", }, logNumber: { float: "left", - width: "3em", + width: "2em", textAlign: "right", paddingRight: "1em", }, logEntry: { lineBreak: "anywhere", - paddingLeft: "3em", + paddingLeft: "2em", whiteSpace: "break-spaces", userSelect: "text", }, @@ -90,6 +109,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 = { @@ -136,36 +171,236 @@ function LogRow({ logRecord, index }) { } function LogPanel(props) { - const { logs, status, value, index, ...other } = props; + const { logs, status, value, index, isOpen, ...other } = props; const classes = useStyles(); + // useStateRef is necessary to resolve the state syncing issue + // https://stackoverflow.com/a/63039797/12208834 + const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef( + true + ); + const liveStreamingRef = useRef(); + const isActive = value === index; + + const handleScroll = _throttle(() => { + const target = document.querySelector("#live-stream-target"); + const scrollBox = document.querySelector("#live-stream-scroll-box"); + const liveStreamTargetVisible = isTargetInsideBox(target, scrollBox); + if (liveStreamTargetVisible !== liveStreamingStateRef.current) { + console.log("live streaming:", liveStreamTargetVisible); + setLiveStreaming(liveStreamTargetVisible); + } + }, 500); + + const scrollToLive = () => { + const liveStreamTarget = document.querySelector("#live-stream-target"); + liveStreamTarget?.scrollIntoView?.({ + behavior: "smooth", + }); + }; + + useEffect(() => { + if (liveStreaming && isActive && status === "BUILDING") { + if (!liveStreamingRef.current) { + scrollToLive(); + } else { + setTimeout(scrollToLive, 100); + } + } + }, [logs, value]); + + useEffect(() => { + if (isActive) { + const liveStreamScrollBox = document.querySelector( + "#live-stream-scroll-box" + ); + liveStreamScrollBox!.addEventListener("scroll", () => { + handleScroll(); + }); + } + }, [value]); + return (