/*
* 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)}>
}
}
);
}