mirror of
https://github.com/rowyio/rowy.git
synced 2026-05-18 13:15:50 +02:00
@@ -7,57 +7,74 @@ import admin from "firebase-admin";
|
||||
|
||||
export default async function generateConfig(
|
||||
schemaPath: string,
|
||||
user: admin.auth.UserRecord
|
||||
user: admin.auth.UserRecord,
|
||||
streamLogger
|
||||
) {
|
||||
return await generateConfigFromTableSchema(schemaPath, user).then(
|
||||
async (success) => {
|
||||
if (!success) {
|
||||
console.log("generateConfigFromTableSchema failed to complete");
|
||||
return await generateConfigFromTableSchema(
|
||||
schemaPath,
|
||||
user,
|
||||
streamLogger
|
||||
).then(async (success) => {
|
||||
if (!success) {
|
||||
await streamLogger.info(
|
||||
`generateConfigFromTableSchema failed to complete`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
await streamLogger.info(`generateConfigFromTableSchema done`);
|
||||
const configFile = fs.readFileSync(
|
||||
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
|
||||
"utf-8"
|
||||
);
|
||||
await streamLogger.info(`configFile: ${JSON.stringify(configFile)}`);
|
||||
const requiredDependencies = configFile.match(
|
||||
/(?<=(require\(("|'))).*?(?=("|')\))/g
|
||||
);
|
||||
if (requiredDependencies) {
|
||||
const packgesAdded = await addPackages(
|
||||
requiredDependencies.map((p: any) => ({ name: p })),
|
||||
user,
|
||||
streamLogger
|
||||
);
|
||||
if (!packgesAdded) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
await streamLogger.info(
|
||||
`requiredDependencies: ${JSON.stringify(requiredDependencies)}`
|
||||
);
|
||||
|
||||
console.log("generateConfigFromTableSchema done");
|
||||
const configFile = fs.readFileSync(
|
||||
path.resolve(__dirname, "../functions/src/functionConfig.ts"),
|
||||
"utf-8"
|
||||
);
|
||||
const requiredDependencies = configFile.match(
|
||||
/(?<=(require\(("|'))).*?(?=("|')\))/g
|
||||
);
|
||||
if (requiredDependencies) {
|
||||
const packgesAdded = await addPackages(
|
||||
requiredDependencies.map((p: any) => ({ name: p })),
|
||||
user
|
||||
);
|
||||
if (!packgesAdded) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const isFunctionConfigValid = await asyncExecute(
|
||||
"cd build/functions/src; tsc functionConfig.ts",
|
||||
commandErrorHandler({
|
||||
const isFunctionConfigValid = await asyncExecute(
|
||||
"cd build/functions/src; tsc functionConfig.ts",
|
||||
commandErrorHandler(
|
||||
{
|
||||
user,
|
||||
functionConfigTs: configFile,
|
||||
description: `Invalid compiled functionConfig.ts`,
|
||||
})
|
||||
);
|
||||
if (!isFunctionConfigValid) {
|
||||
},
|
||||
streamLogger
|
||||
)
|
||||
);
|
||||
await streamLogger.info(
|
||||
`isFunctionConfigValid: ${JSON.stringify(isFunctionConfigValid)}`
|
||||
);
|
||||
if (!isFunctionConfigValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sparksConfig } = require("../functions/src/functionConfig.js");
|
||||
const requiredSparks = sparksConfig.map((s: any) => s.type);
|
||||
await streamLogger.info(
|
||||
`requiredSparks: ${JSON.stringify(requiredSparks)}`
|
||||
);
|
||||
|
||||
for (const lib of requiredSparks) {
|
||||
const success = await addSparkLib(lib, user, streamLogger);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sparksConfig } = require("../functions/src/functionConfig.js");
|
||||
const requiredSparks = sparksConfig.map((s: any) => s.type);
|
||||
console.log({ requiredSparks });
|
||||
|
||||
for (const lib of requiredSparks) {
|
||||
const success = await addSparkLib(lib, user);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,14 +6,22 @@ import { parseSparksConfig } from "../utils";
|
||||
|
||||
export const generateConfigFromTableSchema = async (
|
||||
schemaDocPath: string,
|
||||
user: admin.auth.UserRecord
|
||||
user: admin.auth.UserRecord,
|
||||
streamLogger
|
||||
) => {
|
||||
await streamLogger.info("getting schema...");
|
||||
const schemaDoc = await db.doc(schemaDocPath).get();
|
||||
const schemaData = schemaDoc.data();
|
||||
if (!schemaData) throw new Error("no schema found");
|
||||
|
||||
// Temporarily disabled because this is super long
|
||||
// await streamLogger.info(`schemaData: ${JSON.stringify(schemaData)}`);
|
||||
const derivativeColumns = Object.values(schemaData.columns).filter(
|
||||
(col: any) => col.type === "DERIVATIVE"
|
||||
);
|
||||
await streamLogger.info(
|
||||
`derivativeColumns: ${JSON.stringify(derivativeColumns)}`
|
||||
);
|
||||
const derivativesConfig = `[${derivativeColumns.reduce(
|
||||
(acc, currColumn: any) => {
|
||||
if (
|
||||
@@ -37,11 +45,16 @@ export const generateConfigFromTableSchema = async (
|
||||
},
|
||||
""
|
||||
)}]`;
|
||||
await streamLogger.info(
|
||||
`derivativesConfig: ${JSON.stringify(derivativesConfig)}`
|
||||
);
|
||||
|
||||
const initializableColumns = Object.values(
|
||||
schemaData.columns
|
||||
).filter((col: any) => Boolean(col.config?.defaultValue));
|
||||
console.log(JSON.stringify({ initializableColumns }));
|
||||
await streamLogger.info(
|
||||
`initializableColumns: ${JSON.stringify(initializableColumns)}`
|
||||
);
|
||||
const initializeConfig = `[${initializableColumns.reduce(
|
||||
(acc, currColumn: any) => {
|
||||
if (currColumn.config.defaultValue.type === "static") {
|
||||
@@ -66,6 +79,9 @@ export const generateConfigFromTableSchema = async (
|
||||
},
|
||||
""
|
||||
)}]`;
|
||||
await streamLogger.info(
|
||||
`initializeConfig: ${JSON.stringify(initializeConfig)}`
|
||||
);
|
||||
const documentSelectColumns = Object.values(schemaData.columns).filter(
|
||||
(col: any) => col.type === "DOCUMENT_SELECT" && col.config?.trackedFields
|
||||
);
|
||||
@@ -79,8 +95,12 @@ export const generateConfigFromTableSchema = async (
|
||||
},
|
||||
""
|
||||
)}]`;
|
||||
await streamLogger.info(
|
||||
`documentSelectColumns: ${JSON.stringify(documentSelectColumns)}`
|
||||
);
|
||||
|
||||
const sparksConfig = parseSparksConfig(schemaData.sparks, user);
|
||||
const sparksConfig = parseSparksConfig(schemaData.sparks, user, streamLogger);
|
||||
await streamLogger.info(`sparksConfig: ${JSON.stringify(sparksConfig)}`);
|
||||
|
||||
const collectionType = schemaDocPath.includes("subTables")
|
||||
? "subCollection"
|
||||
@@ -132,6 +152,7 @@ export const generateConfigFromTableSchema = async (
|
||||
default:
|
||||
break;
|
||||
}
|
||||
await streamLogger.info(`collectionType: ${JSON.stringify(collectionType)}`);
|
||||
|
||||
// generate field types from table meta data
|
||||
const fieldTypes = JSON.stringify(
|
||||
@@ -147,6 +168,7 @@ export const generateConfigFromTableSchema = async (
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
await streamLogger.info(`fieldTypes: ${JSON.stringify(fieldTypes)}`);
|
||||
|
||||
const exports: any = {
|
||||
fieldTypes,
|
||||
@@ -157,10 +179,12 @@ export const generateConfigFromTableSchema = async (
|
||||
documentSelectConfig,
|
||||
sparksConfig,
|
||||
};
|
||||
await streamLogger.info(`exports: ${JSON.stringify(exports)}`);
|
||||
|
||||
const fileData = Object.keys(exports).reduce((acc, currKey) => {
|
||||
return `${acc}\nexport const ${currKey} = ${exports[currKey]}`;
|
||||
}, ``);
|
||||
await streamLogger.info(`fileData: ${JSON.stringify(fileData)}`);
|
||||
|
||||
const path = require("path");
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -21,7 +21,8 @@ export const asyncExecute = async (command: string, callback: any) =>
|
||||
|
||||
export const addPackages = async (
|
||||
packages: { name: string; version?: string }[],
|
||||
user: admin.auth.UserRecord
|
||||
user: admin.auth.UserRecord,
|
||||
streamLogger
|
||||
) => {
|
||||
const packagesString = packages.reduce((acc, currPackage) => {
|
||||
return `${acc} ${currPackage.name}@${currPackage.version ?? "latest"}`;
|
||||
@@ -29,10 +30,13 @@ export const addPackages = async (
|
||||
if (packagesString.trim().length !== 0) {
|
||||
const success = await asyncExecute(
|
||||
`cd build/functions;yarn add ${packagesString}`,
|
||||
commandErrorHandler({
|
||||
user,
|
||||
description: "Error adding packages",
|
||||
})
|
||||
commandErrorHandler(
|
||||
{
|
||||
user,
|
||||
description: "Error adding packages",
|
||||
},
|
||||
streamLogger
|
||||
)
|
||||
);
|
||||
return success;
|
||||
}
|
||||
@@ -41,7 +45,8 @@ export const addPackages = async (
|
||||
|
||||
export const addSparkLib = async (
|
||||
name: string,
|
||||
user: admin.auth.UserRecord
|
||||
user: admin.auth.UserRecord,
|
||||
streamLogger
|
||||
) => {
|
||||
try {
|
||||
const { dependencies } = require(`../sparksLib/${name}`);
|
||||
@@ -49,24 +54,30 @@ export const addSparkLib = async (
|
||||
name: key,
|
||||
version: dependencies[key],
|
||||
}));
|
||||
const success = await addPackages(packages, user);
|
||||
const success = await addPackages(packages, user, streamLogger);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
logErrorToDB({
|
||||
user,
|
||||
errorDescription: "Error parsing dependencies",
|
||||
});
|
||||
logErrorToDB(
|
||||
{
|
||||
user,
|
||||
errorDescription: "Error parsing dependencies",
|
||||
},
|
||||
streamLogger
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await asyncExecute(
|
||||
`cp build/sparksLib/${name}.ts build/functions/src/sparks/${name}.ts`,
|
||||
commandErrorHandler({
|
||||
user,
|
||||
description: "Error copying sparksLib",
|
||||
})
|
||||
commandErrorHandler(
|
||||
{
|
||||
user,
|
||||
description: "Error copying sparksLib",
|
||||
},
|
||||
streamLogger
|
||||
)
|
||||
);
|
||||
return success;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const cors = require("cors");
|
||||
import { asyncExecute } from "./compiler/terminal";
|
||||
import { createStreamLogger } from "./utils";
|
||||
import generateConfig from "./compiler";
|
||||
import { auth } from "./firebaseConfig";
|
||||
import meta from "./package.json";
|
||||
@@ -73,29 +74,37 @@ app.post("/", jsonParser, async (req: any, res: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
const success = await generateConfig(configPath, user);
|
||||
const streamLogger = await createStreamLogger(configPath);
|
||||
await streamLogger.info("streamLogger created");
|
||||
|
||||
const success = await generateConfig(configPath, user, streamLogger);
|
||||
if (!success) {
|
||||
console.log(`generateConfig failed to complete`);
|
||||
await streamLogger.error("generateConfig failed to complete");
|
||||
await streamLogger.fail();
|
||||
res.send({
|
||||
success: false,
|
||||
reason: `generateConfig failed to complete`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("generateConfig done");
|
||||
await streamLogger.info("generateConfig success");
|
||||
|
||||
let hasEnvError = false;
|
||||
|
||||
if (!process.env._PROJECT_ID) {
|
||||
await logErrorToDB({
|
||||
errorDescription: `Invalid env: _PROJECT_ID (${process.env._PROJECT_ID})`,
|
||||
user,
|
||||
});
|
||||
await logErrorToDB(
|
||||
{
|
||||
errorDescription: `Invalid env: _PROJECT_ID (${process.env._PROJECT_ID})`,
|
||||
user,
|
||||
},
|
||||
streamLogger
|
||||
);
|
||||
hasEnvError = true;
|
||||
}
|
||||
|
||||
if (hasEnvError) {
|
||||
await streamLogger.error("Invalid env:_PROJECT_ID");
|
||||
await streamLogger.fail();
|
||||
res.send({
|
||||
success: false,
|
||||
reason: "Invalid env:_PROJECT_ID",
|
||||
@@ -106,7 +115,7 @@ app.post("/", jsonParser, async (req: any, res: any) => {
|
||||
await asyncExecute(
|
||||
`cd build/functions; \
|
||||
yarn install`,
|
||||
commandErrorHandler({ user })
|
||||
commandErrorHandler({ user }, streamLogger)
|
||||
);
|
||||
|
||||
await asyncExecute(
|
||||
@@ -114,10 +123,10 @@ app.post("/", jsonParser, async (req: any, res: any) => {
|
||||
yarn deployFT \
|
||||
--project ${process.env._PROJECT_ID} \
|
||||
--only functions`,
|
||||
commandErrorHandler({ user })
|
||||
commandErrorHandler({ user }, streamLogger)
|
||||
);
|
||||
|
||||
console.log("build complete");
|
||||
await streamLogger.success();
|
||||
res.send({
|
||||
success: true,
|
||||
});
|
||||
|
||||
@@ -16,13 +16,36 @@ async function insertErrorRecordToDB(errorRecord: object) {
|
||||
await db.collection("_FT_ERRORS").add(errorRecord);
|
||||
}
|
||||
|
||||
function commandErrorHandler(meta: {
|
||||
user: admin.auth.UserRecord;
|
||||
description?: string;
|
||||
functionConfigTs?: string;
|
||||
sparksConfig?: string;
|
||||
}) {
|
||||
async function insertErrorToStreamer(errorRecord: object, streamLogger) {
|
||||
let errorString = "";
|
||||
for (const key of [
|
||||
"command",
|
||||
"description",
|
||||
"functionConfigTs",
|
||||
"sparksConfig",
|
||||
"stderr",
|
||||
"errorStackTrace",
|
||||
]) {
|
||||
const value = errorRecord[key];
|
||||
if (value) {
|
||||
errorString += `\n\n${key}: ${value}`;
|
||||
}
|
||||
}
|
||||
await streamLogger.error(errorString);
|
||||
}
|
||||
|
||||
function commandErrorHandler(
|
||||
meta: {
|
||||
user: admin.auth.UserRecord;
|
||||
description?: string;
|
||||
functionConfigTs?: string;
|
||||
sparksConfig?: string;
|
||||
},
|
||||
streamLogger
|
||||
) {
|
||||
return async function (error, stdout, stderr) {
|
||||
await streamLogger.info(stdout);
|
||||
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
@@ -39,17 +62,21 @@ function commandErrorHandler(meta: {
|
||||
functionConfigTs: meta?.functionConfigTs ?? "",
|
||||
sparksConfig: meta?.sparksConfig ?? "",
|
||||
};
|
||||
await insertErrorToStreamer(errorRecord, streamLogger);
|
||||
insertErrorRecordToDB(errorRecord);
|
||||
};
|
||||
}
|
||||
|
||||
async function logErrorToDB(data: {
|
||||
errorDescription: string;
|
||||
errorExtraInfo?: string;
|
||||
errorTraceStack?: string;
|
||||
user: admin.auth.UserRecord;
|
||||
sparksConfig?: string;
|
||||
}) {
|
||||
async function logErrorToDB(
|
||||
data: {
|
||||
errorDescription: string;
|
||||
errorExtraInfo?: string;
|
||||
errorTraceStack?: string;
|
||||
user: admin.auth.UserRecord;
|
||||
sparksConfig?: string;
|
||||
},
|
||||
streamLogger?
|
||||
) {
|
||||
console.error(data.errorDescription);
|
||||
|
||||
const errorRecord = {
|
||||
@@ -61,13 +88,16 @@ async function logErrorToDB(data: {
|
||||
errorExtraInfo: data?.errorExtraInfo ?? "",
|
||||
errorStackTrace: data?.errorTraceStack ?? "",
|
||||
};
|
||||
|
||||
if (streamLogger) {
|
||||
await insertErrorToStreamer(errorRecord, streamLogger);
|
||||
}
|
||||
insertErrorRecordToDB(errorRecord);
|
||||
}
|
||||
|
||||
function parseSparksConfig(
|
||||
sparks: string | undefined,
|
||||
user: admin.auth.UserRecord
|
||||
user: admin.auth.UserRecord,
|
||||
streamLogger
|
||||
) {
|
||||
if (sparks) {
|
||||
try {
|
||||
@@ -76,16 +106,81 @@ function parseSparksConfig(
|
||||
.replace(/^(\s*)sparks.config\(/, "")
|
||||
.replace(/\);?\s*$/, "");
|
||||
} catch (error) {
|
||||
logErrorToDB({
|
||||
errorDescription: "Sparks is not wrapped with sparks.config",
|
||||
errorTraceStack: error.stack,
|
||||
user,
|
||||
sparksConfig: sparks,
|
||||
});
|
||||
logErrorToDB(
|
||||
{
|
||||
errorDescription: "Sparks is not wrapped with sparks.config",
|
||||
errorTraceStack: error.stack,
|
||||
user,
|
||||
sparksConfig: sparks,
|
||||
},
|
||||
streamLogger
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return "[]";
|
||||
}
|
||||
|
||||
export { commandErrorHandler, logErrorToDB, parseSparksConfig };
|
||||
async function createStreamLogger(tableConfigPath: string) {
|
||||
const startTimeStamp = Date.now();
|
||||
const fullLog: {
|
||||
log: string;
|
||||
level: "info" | "error";
|
||||
timestamp: number;
|
||||
}[] = [];
|
||||
const logRef = db
|
||||
.doc(tableConfigPath)
|
||||
.collection("ftBuildLogs")
|
||||
.doc(startTimeStamp.toString());
|
||||
await logRef.set({ startTimeStamp, status: "BUILDING" });
|
||||
|
||||
console.log(
|
||||
`streamLogger created. tableConfigPath: ${tableConfigPath}, startTimeStamp: ${startTimeStamp}`
|
||||
);
|
||||
|
||||
return {
|
||||
info: async (log: string) => {
|
||||
console.log(log);
|
||||
fullLog.push({
|
||||
log,
|
||||
level: "info",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
await logRef.update({
|
||||
fullLog,
|
||||
});
|
||||
},
|
||||
error: async (log: string) => {
|
||||
console.error(log);
|
||||
fullLog.push({
|
||||
log,
|
||||
level: "error",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
await logRef.update({
|
||||
fullLog,
|
||||
});
|
||||
},
|
||||
success: async () => {
|
||||
console.log("streamLogger marked as SUCCESS");
|
||||
await logRef.update({
|
||||
status: "SUCCESS",
|
||||
successTimeStamp: Date.now(),
|
||||
});
|
||||
},
|
||||
fail: async () => {
|
||||
console.log("streamLogger marked as FAIL");
|
||||
await logRef.update({
|
||||
status: "FAIL",
|
||||
failTimeStamp: Date.now(),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
commandErrorHandler,
|
||||
logErrorToDB,
|
||||
parseSparksConfig,
|
||||
createStreamLogger,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@monaco-editor/react": "^4.1.0",
|
||||
"@tinymce/tinymce-react": "^3.4.0",
|
||||
"algoliasearch": "^4.8.6",
|
||||
"ansi-to-react": "^6.1.5",
|
||||
"chroma-js": "^2.1.0",
|
||||
"csv-parse": "^4.15.3",
|
||||
"date-fns": "^2.19.0",
|
||||
@@ -30,6 +31,7 @@
|
||||
"json2csv": "^5.0.6",
|
||||
"jszip": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react": "^16.9.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
|
||||
@@ -64,24 +64,6 @@ const useStyles = makeStyles((theme) =>
|
||||
|
||||
...theme.typography.body1,
|
||||
|
||||
// https://codepen.io/evank/pen/wWbRNO
|
||||
background: `
|
||||
linear-gradient(
|
||||
var(--bg-paper) 50%,
|
||||
${fade(theme.palette.background.paper, 0)}
|
||||
),
|
||||
linear-gradient(
|
||||
${fade(theme.palette.background.paper, 0)},
|
||||
var(--bg-paper) 50%
|
||||
) 0 100%,
|
||||
linear-gradient(
|
||||
to top, ${theme.palette.divider} 1px,
|
||||
${fade(theme.palette.divider, 0)}
|
||||
),
|
||||
linear-gradient(to top,
|
||||
${theme.palette.divider} 1px,
|
||||
${fade(theme.palette.divider, 0)}
|
||||
) 0 calc(100% - 0.5px)`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundColor: "var(--bg-paper)",
|
||||
backgroundSize: "100% 2px, 100% 3px, 100% 1px, 100% 1px",
|
||||
|
||||
@@ -142,15 +142,17 @@ export default function SparksEditor() {
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary:showForceSave ? {
|
||||
children: "Force Save",
|
||||
onClick: handleSave,
|
||||
|
||||
}:{
|
||||
children: "Save Changes",
|
||||
onClick: handleSave,
|
||||
disabled:!isSparksValid || localSparks === tableState?.config.sparks ,
|
||||
},
|
||||
primary: showForceSave
|
||||
? {
|
||||
children: "Force Save",
|
||||
onClick: handleSave,
|
||||
}
|
||||
: {
|
||||
children: "Save Changes",
|
||||
onClick: handleSave,
|
||||
disabled:
|
||||
!isSparksValid || localSparks === tableState?.config.sparks,
|
||||
},
|
||||
secondary: {
|
||||
children: "Cancel",
|
||||
onClick: handleClose,
|
||||
|
||||
256
www/src/components/Table/TableHeader/TableLogs.tsx
Normal file
256
www/src/components/Table/TableHeader/TableLogs.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useState } from "react";
|
||||
import useRouter from "hooks/useRouter";
|
||||
import useTable from "hooks/useFiretable/useTable";
|
||||
import { useFiretableContext } from "contexts/FiretableContext";
|
||||
|
||||
import _camelCase from "lodash/camelCase";
|
||||
import _get from "lodash/get";
|
||||
import _find from "lodash/find";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import moment from "moment";
|
||||
|
||||
import {
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
} 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 TableHeaderButton from "./TableHeaderButton";
|
||||
import Ansi from "ansi-to-react";
|
||||
import EmptyState from "components/EmptyState";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
id: `vertical-tab-${index}`,
|
||||
"aria-controls": `vertical-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: "flex",
|
||||
height: `calc(100vh - 200px)`,
|
||||
},
|
||||
tabs: {
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
tab: {
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
alignItems: "center",
|
||||
justifyItems: "center",
|
||||
},
|
||||
|
||||
logPanel: {
|
||||
width: "100%",
|
||||
},
|
||||
logEntryWrapper: {
|
||||
overflowY: "scroll",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
logNumber: {
|
||||
float: "left",
|
||||
width: "3em",
|
||||
textAlign: "right",
|
||||
paddingRight: "1em",
|
||||
},
|
||||
logEntry: {
|
||||
lineBreak: "anywhere",
|
||||
paddingLeft: "3em",
|
||||
whiteSpace: "break-spaces",
|
||||
userSelect: "text",
|
||||
},
|
||||
logTypeInfo: {
|
||||
color: "green",
|
||||
},
|
||||
logTypeError: {
|
||||
color: "red",
|
||||
},
|
||||
}));
|
||||
|
||||
LogPanel.propTypes = {
|
||||
logs: PropTypes.array,
|
||||
status: PropTypes.string,
|
||||
index: PropTypes.any.isRequired,
|
||||
value: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
function LogRow({ logRecord, index }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body2" className={classes.logNumber}>
|
||||
{index}
|
||||
</Typography>
|
||||
<Typography variant="body2" className={classes.logEntry}>
|
||||
<Ansi
|
||||
className={
|
||||
logRecord.level === "info"
|
||||
? classes.logTypeInfo
|
||||
: classes.logTypeError
|
||||
}
|
||||
>
|
||||
{moment(logRecord.timestamp).format("LTS")}
|
||||
</Ansi>
|
||||
{" "}
|
||||
<Ansi>
|
||||
{logRecord.log.replaceAll("\\n", "\n").replaceAll("\\t", "\t")}
|
||||
</Ansi>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function LogPanel(props) {
|
||||
const { logs, status, value, index, ...other } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`vertical-tabpanel-${index}`}
|
||||
aria-labelledby={`vertical-tab-${index}`}
|
||||
className={classes.logPanel}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box p={3} className={classes.logEntryWrapper}>
|
||||
{logs?.map((log, index) => {
|
||||
return <LogRow logRecord={log} index={index} />;
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TableLogs() {
|
||||
const router = useRouter();
|
||||
const { tableState } = useFiretableContext();
|
||||
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
const ftBuildStreamID =
|
||||
"_FIRETABLE_/settings/schema/" +
|
||||
tableCollection
|
||||
.split("/")
|
||||
.filter(function (_, i) {
|
||||
// replace IDs with subTables that appears at even indexes
|
||||
return i % 2 === 0;
|
||||
})
|
||||
.join("/subTables/");
|
||||
|
||||
const [collectionState] = useTable({
|
||||
path: `${ftBuildStreamID}/ftBuildLogs`,
|
||||
orderBy: [{ key: "startTimeStamp", direction: "desc" }],
|
||||
});
|
||||
const latestStatus = collectionState?.rows?.[0]?.status;
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setTabIndex(newValue);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderButton
|
||||
title="Build Logs"
|
||||
onClick={() => setOpen(true)}
|
||||
icon={
|
||||
<>
|
||||
{latestStatus === "BUILDING" && <CircularProgress size={20} />}
|
||||
{latestStatus === "SUCCESS" && <SuccessIcon />}
|
||||
{latestStatus === "FAIL" && <FailIcon />}
|
||||
{!latestStatus && <LogsIcon />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{open && !!tableState && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
maxWidth="xl"
|
||||
fullWidth
|
||||
title={
|
||||
<>
|
||||
Build Logs <Chip label="ALPHA" size="small" />
|
||||
</>
|
||||
}
|
||||
children={
|
||||
<>
|
||||
{!latestStatus && (
|
||||
<EmptyState
|
||||
message="No Logs Found"
|
||||
description={
|
||||
"When you start building, your logs should be shown here shortly"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{latestStatus && (
|
||||
<div className={classes.root}>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tabIndex}
|
||||
onChange={handleTabChange}
|
||||
className={classes.tabs}
|
||||
>
|
||||
{collectionState.rows.map((logEntry, index) => (
|
||||
<Tab
|
||||
label={
|
||||
<Box className={classes.tab}>
|
||||
<Box>
|
||||
{moment(logEntry.startTimeStamp).format(
|
||||
"MMMM D YYYY h:mm:ssa"
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{logEntry.status === "BUILDING" && (
|
||||
<CircularProgress size={24} />
|
||||
)}
|
||||
{logEntry.status === "SUCCESS" && <SuccessIcon />}
|
||||
{logEntry.status === "FAIL" && <FailIcon />}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
{...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
{collectionState.rows.map((logEntry, index) => (
|
||||
<LogPanel
|
||||
value={tabIndex}
|
||||
index={index}
|
||||
logs={logEntry?.fullLog}
|
||||
status={logEntry?.status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import Filters from "../Filters";
|
||||
import ImportCSV from "./ImportCsv";
|
||||
import Export from "./Export";
|
||||
import TableSettings from "./TableSettings";
|
||||
import TableLogs from "./TableLogs";
|
||||
import HiddenFields from "../HiddenFields";
|
||||
import Sparks from "./Sparks";
|
||||
import ReExecute from "./ReExecute";
|
||||
@@ -219,6 +220,10 @@ export default function TableHeader({
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<TableLogs />
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<TableSettings />
|
||||
</Grid>
|
||||
|
||||
@@ -3109,6 +3109,11 @@ alphanum-sort@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
|
||||
|
||||
anser@^1.4.1:
|
||||
version "1.4.10"
|
||||
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b"
|
||||
integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==
|
||||
|
||||
ansi-align@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
|
||||
@@ -3192,6 +3197,14 @@ ansi-styles@^4.1.0:
|
||||
"@types/color-name" "^1.1.1"
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-to-react@^6.1.5:
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ansi-to-react/-/ansi-to-react-6.1.5.tgz#00ca9e4f566b468f0aa6a213f34b752b3e7121a9"
|
||||
integrity sha512-u7fkGMK+p6keOtgExruUDVmEnTBAWizlHJXzx9eJxw4rCVE6omzm0Gw4BQmB9oUPr0WriYf7Wyprms8uL8eL5Q==
|
||||
dependencies:
|
||||
anser "^1.4.1"
|
||||
escape-carriage "^1.3.0"
|
||||
|
||||
ansicolors@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
|
||||
@@ -5921,6 +5934,11 @@ escalade@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-carriage@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-carriage/-/escape-carriage-1.3.0.tgz#71006b2d4da8cb6828686addafcb094239c742f3"
|
||||
integrity sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==
|
||||
|
||||
escape-goat@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
|
||||
@@ -10136,6 +10154,11 @@ mkdirp@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
monaco-editor@^0.21.2:
|
||||
version "0.21.2"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.21.2.tgz#37054e63e480d51a2dd17d609dcfb192304d5605"
|
||||
|
||||
Reference in New Issue
Block a user