Merge branch 'develop' of https://github.com/FiretableProject/firetable into develop

This commit is contained in:
Shams mosowi
2021-06-19 12:09:59 +10:00
5 changed files with 308 additions and 25 deletions

View File

@@ -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",

View File

@@ -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: {

View File

@@ -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<any>();
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 (
<div
role="tabpanel"
hidden={value !== index}
hidden={!isActive}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
className={classes.logPanel}
{...other}
>
{value === index && (
<Box p={3} className={classes.logEntryWrapper}>
<Box
p={3}
className={classes.logEntryWrapper}
id="live-stream-scroll-box"
>
{logs?.map((log, index) => {
return <LogRow logRecord={log} index={index} />;
return <LogRow logRecord={log} index={index} key={index} />;
})}
<div ref={liveStreamingRef} id="live-stream-target">
{status === "BUILDING" && (
<CircularProgress
className={classes.logPanelProgress}
size={30}
/>
)}
</div>
<div style={{ height: 10 }} />
</Box>
)}
</div>
);
}
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<any>();
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 (
<Box
className={`${classes.snackLog} ${expanded && classes.snackLogExpanded}`}
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="overline">
{!log && <span>Build Pending...</span>}
{log?.status === "SUCCESS" && (
<span
style={{
color: "#aed581",
}}
>
Build Completed
</span>
)}
{log?.status === "FAIL" && (
<span
style={{
color: "#e57373",
}}
>
Build Failed
</span>
)}
{log?.status === "BUILDING" && <span>Building...</span>}
</Typography>
<Box>
<IconButton
aria-label="expand"
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <CollapseIcon /> : <ExpandIcon />}
</IconButton>
<IconButton aria-label="open" size="small" onClick={onOpenPanel}>
<OpenIcon />
</IconButton>
<IconButton aria-label="close" size="small" onClick={onClose}>
<CloseIcon />
</IconButton>
</Box>
</Box>
<Box
className={classes.logEntryWrapper}
height={"calc(100% - 25px)"}
id="live-stream-scroll-box-snack"
>
{log && (
<>
{logs?.map((log, index) => {
return <LogRow logRecord={log} index={index} key={index} />;
})}
<div ref={liveStreamingRef} id="live-stream-target-snack">
{status === "BUILDING" && (
<CircularProgress
className={classes.logPanelProgress}
size={30}
/>
)}
</div>
<div style={{ height: 10 }} />
</>
)}
</Box>
</Box>
);
}
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 [buildURLConfigured, setBuildURLConfigured] = useState(true);
const [tabIndex, setTabIndex] = React.useState(0);
const [activeLogTimestamp, setActiveLogTimestamp] = useState(Date.now());
useEffect(() => {
checkBuildURL();
}, []);
const checkBuildURL = async () => {
const settingsDoc = await db.doc("/_FIRETABLE_/settings").get();
const ftBuildUrl = settingsDoc.get("ftBuildUrl");
if (!ftBuildUrl) {
setBuildURLConfigured(false);
}
};
useEffect(() => {
if (requestSnackLog > 0) {
setTimeout(() => {
setActiveLogTimestamp(requestSnackLog);
setSnackOpen(true);
}, 500);
}
}, [requestSnackLog]);
const tableCollection = decodeURIComponent(router.match.params.id);
const ftBuildStreamID =
@@ -182,21 +417,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 (
<>
<TableHeaderButton
title="Build Logs"
onClick={() => setOpen(true)}
onClick={() => setPanelOpen(true)}
icon={
<>
{latestStatus === "BUILDING" && <CircularProgress size={20} />}
@@ -207,9 +441,21 @@ export default function TableLogs() {
}
/>
{open && !!tableState && (
{snackOpen && (
<SnackLog
log={latestActiveLog}
onClose={() => setSnackOpen(false)}
onOpenPanel={() => {
setPanelOpen(true);
}}
/>
)}
{panalOpen && !!tableState && (
<Modal
onClose={handleClose}
onClose={() => {
setPanelOpen(false);
}}
maxWidth="xl"
fullWidth
title={
@@ -219,7 +465,7 @@ export default function TableLogs() {
}
children={
<>
{!latestStatus && (
{!latestStatus && buildURLConfigured && (
<EmptyState
message="No Logs Found"
description={
@@ -227,6 +473,27 @@ export default function TableLogs() {
}
/>
)}
{!latestStatus && !buildURLConfigured && (
<EmptyState
message="Need Configuration"
description={
<>
Cloud Run trigger URL not configured. Configuration guide:{" "}
<Link
href={
"https://github.com/AntlerVC/firetable/wiki/Setting-up-cloud-Run-FT-Builder"
}
target="_blank"
rel="noopener noreferrer"
variant="body2"
underline="always"
>
https://github.com/AntlerVC/firetable/wiki/Setting-up-cloud-Run-FT-Builder
</Link>
</>
}
/>
)}
{latestStatus && (
<div className={classes.root}>
<Tabs
@@ -238,6 +505,7 @@ export default function TableLogs() {
>
{collectionState.rows.map((logEntry, index) => (
<Tab
key={index}
label={
<Box className={classes.tab}>
<Box>
@@ -260,6 +528,7 @@ export default function TableLogs() {
</Tabs>
{collectionState.rows.map((logEntry, index) => (
<LogPanel
key={index}
value={tabIndex}
index={index}
logs={logEntry?.fullLog}

View File

@@ -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") && (
<Grid item>
<Sparks />
<Sparks requestSnackLog={openSnackLog} />
</Grid>
)}
{userClaims?.roles?.includes("ADMIN") && (
<Grid item>
<TableLogs requestSnackLog={snackCount} />
</Grid>
)}
@@ -220,10 +231,6 @@ export default function TableHeader({
</Grid>
)}
<Grid item>
<TableLogs />
</Grid>
<Grid item>
<TableSettings />
</Grid>

View File

@@ -12685,6 +12685,11 @@ react-transition-group@^4.0.0, react-transition-group@^4.4.0:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-usestateref@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.5.tgz#0b5659217022a7e88087875ec1745730940b7882"
integrity sha512-Bia+nJDuhmakaUQR6ztVPvZnGz2cV8ZOh9Tlgpc2iZorwc2Li4xcQUICtMwdAms5N9bH8FnPX+mXn91EsExvvg==
"react@^16.8.0 || ^17.0.0":
version "17.0.1"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"