/* * QQQ - Low-code Application Framework for Engineers. * Copyright (C) 2021-2022. Kingsrook, LLC * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States * contact@kingsrook.com * https://github.com/Kingsrook/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {Chip, SelectChangeEvent} from "@mui/material"; import Alert from "@mui/material/Alert"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Divider from "@mui/material/Divider"; import FormControl from "@mui/material/FormControl/FormControl"; import Grid from "@mui/material/Grid"; import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import ListItemAvatar from "@mui/material/ListItemAvatar"; import ListItemText from "@mui/material/ListItemText"; import MenuItem from "@mui/material/MenuItem"; import Modal from "@mui/material/Modal"; import Select from "@mui/material/Select/Select"; import Snackbar from "@mui/material/Snackbar"; import Tab from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import Typography from "@mui/material/Typography"; import TabPanel from "qqq/components/misc/TabPanel"; import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm"; import ScriptEditor, {ScriptEditorProps} from "qqq/components/scripts/ScriptEditor"; import ScriptLogsView from "qqq/components/scripts/ScriptLogsView"; import ScriptTestForm from "qqq/components/scripts/ScriptTestForm"; import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import {LoadingState} from "qqq/models/LoadingState"; import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils"; import Client from "qqq/utils/qqq/Client"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; import "ace-builds/src-noconflict/mode-java"; import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-velocity"; import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/theme-github"; import React, {useReducer, useState} from "react"; import AceEditor from "react-ace"; import {Link} from "react-router-dom"; import "ace-builds/src-noconflict/ext-language_tools"; const qController = Client.getInstance(); // Declaring props types for ViewForm interface Props { scriptId: number, associatedScriptTableName?: string, associatedScriptFieldName?: string, associatedScriptRecordId?: any, testInputFields?: QFieldMetaData[], testOutputFields?: QFieldMetaData[], } ScriptViewer.defaultProps = { associatedScriptTableName: null, associatedScriptFieldName: null, associatedScriptRecordId: null, testInputFields: null, testOutputFields: null, }; export default function ScriptViewer({scriptId, associatedScriptTableName, associatedScriptFieldName, associatedScriptRecordId, testInputFields, testOutputFields}: Props): JSX.Element { const [metaData, setMetaData] = useState(null as QInstance); const [scriptRecord, setScriptRecord] = useState(null as QRecord); const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [versionRecordList, setVersionRecordList] = useState(null as QRecord[]); const [selectedVersionRecord, setSelectedVersionRecord] = useState(null as QRecord); const [scriptLogs, setScriptLogs] = useState({} as any); const [scriptTypeRecord, setScriptTypeRecord] = useState(null as QRecord); const [scriptTypeFileSchemaList, setScriptTypeFileSchemaList] = useState(null as QRecord[]); const [availableFileNames, setAvailableFileNames] = useState([] as string[]); const [selectedFileName, setSelectedFileName] = useState(""); const [currentVersionId, setCurrentVersionId] = useState(null as number); const [notFoundMessage, setNotFoundMessage] = useState(null); const [selectedTab, setSelectedTab] = useState(0); const [editorProps, setEditorProps] = useState(null as ScriptEditorProps); const [successText, setSuccessText] = useState(null as string); const [failText, setFailText] = useState(null as string); const [, forceUpdate] = useReducer((x) => x + 1, 0); const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading")); if (!asyncLoadInited) { setAsyncLoadInited(true); (async () => { try { setMetaData(await qController.loadMetaData()); const scriptRecord = await qController.get("script", scriptId); setScriptRecord(scriptRecord); const scriptTypeRecord = await qController.get("scriptType", scriptRecord.values.get("scriptTypeId")); setScriptTypeRecord(scriptTypeRecord); let fileMode = scriptTypeRecord.values.get("fileMode"); let scriptTypeFileSchemaList: QRecord[] = null; if (fileMode == 1) // SINGLE { scriptTypeFileSchemaList = [new QRecord({values: {name: "Script.js", fileType: "javascript"}})]; } else if (fileMode == 2) // MULTI_PRE_DEFINED { const filter = new QQueryFilter([new QFilterCriteria("scriptTypeId", QCriteriaOperator.EQUALS, [scriptRecord.values.get("scriptTypeId")])], [new QFilterOrderBy("id")]); scriptTypeFileSchemaList = await qController.query("scriptTypeFileSchema", filter); } else // MULTI AD_HOC { // todo - not yet supported console.log(`Script Type File Mode of ${fileMode} is not yet supported.`); } setScriptTypeFileSchemaList(scriptTypeFileSchemaList); if (scriptTypeFileSchemaList) { const availableFileNames = scriptTypeFileSchemaList.map((fileSchemaRecord) => fileSchemaRecord.values.get("name")); setAvailableFileNames(availableFileNames); setSelectedFileName(availableFileNames[0]); } const criteria = [new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, [scriptId])]; const orderBys = [new QFilterOrderBy("sequenceNo", false)]; const filter = new QQueryFilter(criteria, orderBys, null, "AND", 0, 25); const versions = await qController.query("scriptRevision", filter); console.log("Fetched versions:"); console.log(versions); setVersionRecordList(versions); if (versions && versions.length > 0) { selectVersion(versions[0]); } } catch (e) { if (e instanceof QException) { if ((e as QException).status === 404) { setNotFoundMessage("Script code could not be found."); return; } } setNotFoundMessage("Error loading Script code: " + e); } })(); } const editData = (selectedVersionRecord: QRecord) => { const editorProps = {} as ScriptEditorProps; editorProps.title = (selectedVersionRecord ? "Editing Code for Script: " : "Initializing Code for Script: ") + scriptRecord?.values?.get("name"); editorProps.scriptRevisionRecord = selectedVersionRecord; editorProps.scriptId = scriptId; editorProps.tableName = associatedScriptTableName; editorProps.fieldName = associatedScriptFieldName; editorProps.recordId = associatedScriptRecordId; editorProps.scriptTypeRecord = scriptTypeRecord; editorProps.scriptTypeFileSchemaList = scriptTypeFileSchemaList; setEditorProps(editorProps); }; const closeEditingScript = (event: object, reason: string, alert: string = null) => { if (reason === "backdropClick" || reason === "escapeKeyDown") { return; } if (reason === "saved") { setAsyncLoadInited(false); forceUpdate(); if (alert) { setSuccessText(alert); } } else if (reason === "failed") { setAsyncLoadInited(false); forceUpdate(); if (alert) { setFailText(alert); } } setEditorProps(null); }; const changeTab = (newValue: number) => { setSelectedTab(newValue); forceUpdate(); }; const selectVersion = (version: QRecord) => { (async () => { setCurrentVersionId(version.values.get("id")); loadingSelectedVersion.setLoading(); ////////////////////////////////////////////////////////////////////////////////////// // fetch the full version - including its associated scriptRevisionFile sub-records // ////////////////////////////////////////////////////////////////////////////////////// const selectedVersion = await qController.get("scriptRevision", version.values.get("id"), null, true); console.log("Fetched selectedVersion:"); console.log(selectedVersion); setSelectedVersionRecord(selectedVersion); loadingSelectedVersion.setNotLoading(); forceUpdate(); })(); }; const handleSelectFile = (event: SelectChangeEvent) => { setSelectedFileName(event.target.value); }; const getSelectedFileCode = (): string => { return (getSelectedVersionCode()[selectedFileName] ?? ""); }; const getSelectedFileType = (): string => { for (let i = 0; i < scriptTypeFileSchemaList.length; i++) { let name = scriptTypeFileSchemaList[i].values.get("name"); if (name == selectedFileName) { return (scriptTypeFileSchemaList[i].values.get("fileType")); } } return ("javascript"); // have some default... }; const getSelectedVersionCode = (): { [name: string]: string } => { let rs: { [name: string]: string } = {}; let files = selectedVersionRecord?.associatedRecords?.get("files"); for (let j = 0; j < files?.length; j++) { let file = files[j]; rs[file.values.get("fileName")] = file.values.get("contents"); } return (rs); }; function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord) { return { (versionRecordList == null || versionRecordList.length == 0) ? There are not any versions of this script. : <> } { versionRecordList?.map((version: any) => { const timeAuthorLine = `${ValueUtils.formatDateTime(version.values.get("createDate"))} by ${version.values.get("author")}`; const apiLine = `API: ${version.displayValues.get("apiName") ?? "None"} version ${version.displayValues.get("apiVersion") ?? "None"}`; return ( selectVersion(version)}> {`${version.values.get("sequenceNo")}`} {scriptRecord.values.get("currentScriptRevisionId") == version?.values?.get("id") && } {version.values.get("commitMessage")} } secondary={ <>
{timeAuthorLine}
{ (version.displayValues.get("apiName") || version.displayValues.get("apiVersion")) &&
{apiLine}
} } />
); }) }
; } const getScriptLogs = (scriptRevisionId: number) => { if (!scriptLogs[scriptRevisionId]) { (async () => { let filter = new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])], [new QFilterOrderBy("id", false)], null, "AND", 0, 100); scriptLogs[scriptRevisionId] = await qController.query("scriptLog", filter); setScriptLogs(scriptLogs); forceUpdate(); })(); return Loading...; } const logs = scriptLogs[scriptRevisionId] as any[]; if (logs === null || logs === undefined) { return Loading...; } if (logs.length === 0) { return No logs available for this version.; } return (); }; let editButtonTooltip = ""; let editButtonText = "Create New Version"; if (currentVersionId) { if (currentVersionId === scriptRecord?.values?.get("currentScriptRevisionId")) { editButtonTooltip = "If you make any changes to this script, a new version will be created when you hit Save."; editButtonText = "Edit"; } else { editButtonTooltip = "If you want to make this previous version active, bring up the Edit window, make any changes " + "to the old version if they are needed, then click Save. A new version will be created, and set as current."; editButtonText = "Edit and Activate"; } } function buildScriptLogFilter(scriptRevisionId: any) { return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])])); } return ( { notFoundMessage ? {notFoundMessage} : { successText ? ( setSuccessText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}> setSuccessText(null)}> {successText} ) : ("") } { failText ? ( setFailText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}> setFailText(null)}> {failText} ) : ("") } <> changeTab(newValue)} variant="standard" > Versions {getVersionsList(versionRecordList, selectedVersionRecord)} { selectedVersionRecord ? Version {selectedVersionRecord.values.get("sequenceNo")} { currentVersionId === scriptRecord.values.get("currentScriptRevisionId") ? (<> (Current)) : <> } : <> } { loadingSelectedVersion.isNotLoading() && selectedVersionRecord ? ( <> { availableFileNames && availableFileNames.length > 1 && } ) : null } { loadingSelectedVersion.isLoadingSlow() && Loading... } Versions {getVersionsList(versionRecordList, selectedVersionRecord)} { selectedVersionRecord ? ( <> Script Logs (Version {selectedVersionRecord?.values.get("sequenceNo")}) View All {getScriptLogs(selectedVersionRecord.values.get("id"))} ) : Select a version to view logs } { editorProps && closeEditingScript(event, reason)}> } } ); }