Logging & Progressbars Inside Innovator
In Aras, we have a lot of different automations running at any given time, and there is no clean way (I know of) to see them all in one place. Further, I don't have access to the server, so I can't access the log files you might typically see so I'm looking for a way to write those directly into Aras.
So I set out to address these issues, by creating an ItemType called Progressbar with fields for tracking progress and on the form I created a html object and a button to trigger a 'ProgressBarClient' Method, which repeatedly calls a 'ProgressBarServer' Method multiple times.
This gets me an interface to see the status of the processing...
So before I spend too much time on this, I thought I'd see if the community has any ideas on better ways to approach this or ideas for improvement.
Some thoughts:
- Using the button on the item probably wasn't the best move, it stops processing if you close the item. It also gave me a lot of headaches getting the typing right
- I should probably set it up so the C# method doesn't require a return to update the progress
- I was hoping for a live progressbar in the search grid... but using the html on the form isn't going to work because its not a property
// Javascript Client Method: ProgressBarClient
console.group("---Action Trigger---");
// ProgressBar ItemType Values
// barmin - int - minimum value of progressbar
// barmax - int - maximum value of progressbar
// barvalue - int - current value of progressbar
// barpercent - decimal - % complete, barvalue / barmax
// currenttask - string - text description of current action
// debuglog - Text - log of all activity
// errorlog - Text - log of error messages
console.log("Name: ProgressBar Prototype");
console.log("Description: This will call a server side method and update a progressbar");
console.log("");
console.log("Setting up progressbar...");
// --- Instantiate Innovator object at the beginning ---
var inn;
try {
inn = new Innovator();
console.log("Innovator object instantiated successfully.");
} catch (e) {
console.error("CRITICAL ERROR: Failed to instantiate Innovator. The global 'Innovator' might not be a constructor or is not available. Error: " + e.message);
alert("An internal error occurred. Please contact support. (Innovator instantiation failed)");
console.groupEnd();
return;
}
// Basic check to ensure 'inn' is a valid object after instantiation
if (!inn || typeof inn.getItemById !== 'function') {
console.error("CRITICAL ERROR: The 'inn' object does not have the expected Aras API methods (e.g., getItemById) even after instantiation.");
alert("An internal error occurred. Please contact support. (Invalid Innovator object)");
console.groupEnd();
return;
}
console.log("Innovator object retrieved and validated.");
// Get the current Aras Item object (assuming parent.item is available and is an XML DOM node)
var currentArasItemXml = parent.item;
if (!currentArasItemXml) {
console.error("CRITICAL ERROR: Aras Item XML object (parent.item) could not be retrieved.");
alert("An internal error occurred. Please contact support. (Aras Item XML not found)");
console.groupEnd();
return;
}
console.log("Current Aras Item XML object retrieved.");
var itemId = currentArasItemXml.getAttribute('id');
console.log("item ID:" + itemId);
var itemTypeName = currentArasItemXml.getAttribute('type');
console.log("item Type:" + itemTypeName);
if (!itemId || !itemTypeName) {
console.error("CRITICAL ERROR: Item ID or ItemType name not found from parent.item.");
alert("An internal error occurred. Please contact support. (Item ID/Type missing)");
console.groupEnd();
return;
}
var currentArasItem = inn.getItemById(itemTypeName, itemId);
if (currentArasItem.isError()) {
console.error("CRITICAL ERROR: Failed to fetch full Aras Item object: " + currentArasItem.getErrorString());
alert("An internal error occurred. Please contact support. (Failed to fetch item)");
console.groupEnd();
return;
}
console.log("Full Aras Item object fetched.");
// Find the form field control for our property
var propName = "html0"; // This is correct based on your HTML inspection
var fieldContainerEl; // This will be the <div> with name="html0"
var targetHtmlEl; // This will be the inner <div class="sys_f_value">
// --- MODIFIED: Robust way to find the field element without aras.uiFindField ---
console.log("Attempting to find field for property: " + propName + " directly in the DOM...");
// Try to find the element in the current document (most likely the iframe document)
fieldContainerEl = document.getElementsByName(propName)[0];
// If not found, try to find it in the top document (less likely for form fields, but a fallback)
if (!fieldContainerEl) {
try {
fieldContainerEl = top.document.getElementsByName(propName)[0];
} catch (e) {
console.warn("Could not access top.document directly to find field. Error: " + e.message);
}
}
if (!fieldContainerEl) {
console.error("CRITICAL ERROR: Progress bar field '" + propName + "' not found on form. Please ensure the field's HTML 'name' attribute is '" + propName + "' and the form is loaded.");
alert("Progress bar field '" + propName + "' not found on form. Please ensure the field's HTML 'name' attribute is '" + propName + "' and the form is loaded.");
console.groupEnd();
return;
}
console.log("Progress bar field container element found. Name attribute used: " + fieldContainerEl.name);
// Find the inner div with class "sys_f_value" to inject the HTML
targetHtmlEl = fieldContainerEl.querySelector('.sys_f_value');
if (!targetHtmlEl) {
console.error("CRITICAL ERROR: Could not find the inner '.sys_f_value' div within the '" + propName + "' field container.");
alert("An internal error occurred. Please contact support. (Target HTML element for injection not found)");
console.groupEnd();
return;
}
console.log("Target HTML element for injection found.");
// --- END MODIFIED SECTION ---
// Create basic progress bar HTML
function renderBar(percent, currentTask) {
percent = Math.max(0, Math.min(100, percent));
return '' +
'<div style="border:1px solid #999;width:100%;height:18px;background:#f5f5f5;">' +
' <div style="height:100%;width:' + percent + '%;background:#4caf50;transition:width 0.2s;"></div>' +
'</div>' +
'<div style="font-size:10px;margin-top:2px;">' + percent.toFixed(2) + '% ' + (currentTask ? '(' + currentTask + ')' : '') + '</div>';
}
// Helper to set the field value + UI
function setProgress(barvalue, barmax, currenttask) {
var percent = (barmax > 0) ? (barvalue / barmax) * 100 : 0;
var renderedHtml = renderBar(percent, currenttask);
// Update rendered field in current UI
targetHtmlEl.innerHTML = renderedHtml;
}
// Initialize at 0%
setProgress(0, 100, "Initializing...");
console.log("Progress bar initialized to 0%.");
// --- START: Replaced simulated work with Server Method call ---
// Variables to maintain state across server calls
var progressItemId = null; // Stores the ID of the Progressbar item created/updated by the server
var currentStep = 0; // Stores the current step to send to the server
// Function to call the server method and update progress
function callServerMethodForProgress(initialCall) {
console.log("Calling the server-side method 'ProgressBarServer'...");
var serverMethod = inn.newItem("Method", "ProgressBarServer");
serverMethod.setProperty("itemId", itemId); // Pass the ID of the item that triggered this client method
serverMethod.setProperty("itemTypeName", itemTypeName); // Pass the ItemType name
serverMethod.setProperty("initialCall", initialCall ? "true" : "false");
serverMethod.setProperty("progressItemId", progressItemId || ""); // Pass the ID of the Progressbar item
serverMethod.setProperty("currentStep", currentStep.toString()); // Pass the current step
var result = serverMethod.apply();
if (result.isError()) {
var errorMsg = result.getErrorString();
console.error("Server method 'ProgressBarServer' error: " + errorMsg);
alert("Server method error: " + errorMsg);
setProgress(0, 100, "Error: " + errorMsg); // Show error in progress bar
console.groupEnd();
return; // Stop progress
}
// Parse the response from the server method
var progressItem = result.getItemByIndex(0);
if (!progressItem || progressItem.isError()) {
console.error("Server method 'ProgressBarServer' returned an invalid or empty item.");
alert("Server method returned invalid data.");
setProgress(0, 100, "Error: Invalid server response");
console.groupEnd();
return;
}
// Extract properties from the server's response
progressItemId = progressItem.getProperty("id", progressItemId); // Update progressItemId if it's the first call
var barmin = parseInt(progressItem.getProperty("barmin", "0"));
var barmax = parseInt(progressItem.getProperty("barmax", "100"));
var barvalue = parseInt(progressItem.getProperty("barvalue", "0"));
var barpercent = parseFloat(progressItem.getProperty("barpercent", "0"));
var currenttask = progressItem.getProperty("currenttask", "Processing...");
var debuglog = progressItem.getProperty("debuglog", "");
var errorlog = progressItem.getProperty("errorlog", "");
var isComplete = progressItem.getProperty("is_complete", "false") === "true";
currentStep = parseInt(progressItem.getProperty("next_step", (currentStep + 1).toString())); // Get next step from server
console.log("Server response - Value: " + barvalue + ", Max: " + barmax + ", Task: " + currenttask + ", Complete: " + isComplete + ", Progress Item ID: " + progressItemId);
if (debuglog) console.log("Server Debug Log:\n" + debuglog);
if (errorlog) console.error("Server Error Log:\n" + errorlog);
// Update the client-side progress bar
setProgress(barvalue, barmax, currenttask);
// Persist the progress to the Progressbar ItemType (if it's the item being edited)
// This assumes the form you are on *is* the Progressbar ItemType form.
// If this client method is on a *different* ItemType's form, you would need to
// load the Progressbar item by its ID and update its properties.
if (itemTypeName === "Progressbar" && itemId === progressItemId) {
currentArasItem.setProperty("barmin", barmin.toString());
currentArasItem.setProperty("barmax", barmax.toString());
currentArasItem.setProperty("barvalue", barvalue.toString());
currentArasItem.setProperty("barpercent", barpercent.toFixed(2));
currentArasItem.setProperty("currenttask", currenttask);
currentArasItem.setProperty("debuglog", debuglog);
currentArasItem.setProperty("errorlog", errorlog);
// Note: This only updates the client-side item object.
// To save to the database, the user would need to click 'Save' on the form,
// or you would need to explicitly call currentArasItem.apply("update") here,
// which might be too frequent and cause performance issues.
// The server method already saves the Progressbar item.
} else {
// If the client method is on a different ItemType, we need to explicitly update the Progressbar item
// This is an example of how to update the Progressbar item from a different context
var updateProgressItem = inn.newItem("Progressbar", "edit");
updateProgressItem.setID(progressItemId);
updateProgressItem.setProperty("barmin", barmin.toString());
updateProgressItem.setProperty("barmax", barmax.toString());
updateProgressItem.setProperty("barvalue", barvalue.toString());
updateProgressItem.setProperty("barpercent", barpercent.toFixed(2));
updateProgressItem.setProperty("currenttask", currenttask);
updateProgressItem.setProperty("debuglog", debuglog);
updateProgressItem.setProperty("errorlog", errorlog);
// updateProgressItem.apply("update"); // Uncomment if you want to save from client, but server already does this.
}
if (!isComplete) {
// If not complete, call the server method again after a short delay
setTimeout(function () {
callServerMethodForProgress(false);
}, 500); // Adjust delay as needed
} else {
inn.showStatusMessage("Processing completed.", "info");
console.log("Processing completed.");
console.groupEnd();
}
}
// Start the process by making the initial call to the server method
callServerMethodForProgress(true);
// --- END: Replaced simulated work ---
console.log("Notify user of completion (handled by showStatusMessage)");// C# Server Method: ProgressBarServer
Innovator innovator = this.getInnovator();
// Get parameters from the client
string progressItemId = this.getProperty("itemId");
string currentStepStr = this.getProperty("currentStep", "0");
int currentStep = int.Parse(currentStepStr);
Item progressBarItem;
int barmin = 0;
int barmax = 10; // Define total steps for the simulated task
// --- Step 1: Load the existing Progressbar Item ---
if (string.IsNullOrEmpty(progressItemId))
{
return innovator.newError("Error: 'progressItemId' is required but was not provided. The Progressbar item must exist.");
}
// Load the item first to get its current state
progressBarItem = innovator.newItem("Progressbar", "get");
progressBarItem.setID(progressItemId);
progressBarItem = progressBarItem.apply();
if (progressBarItem.isError())
{
return innovator.newError("Failed to load Progressbar item (ID: " + progressItemId + "): " + progressBarItem.getErrorString());
}
// Ensure barmax is consistent if it was set on creation
barmax = int.Parse(progressBarItem.getProperty("barmax", barmax.ToString()));
// --- Step 2: Attempt to lock the item for update ---
// Use the loaded item to attempt the lock.
// This is important: applying 'lock' on the loaded item ensures we're working with the same item instance.
Item lockResult = progressBarItem.apply("lock");
if (lockResult.isError())
{
string errorString = lockResult.getErrorString();
// Check for the specific Aras error message indicating it's already locked by *this* user.
// The exact message for "already locked by current user" can vary slightly.
// We'll use a broader check for "locked" and then check the user.
if (errorString.Contains("Item is already locked") || errorString.Contains("Aras.Server.Core.ItemIsAlreadyLockedException"))
{
// Now, check *who* locked it.
// Get the locked_by property from the item (if it exists and is populated)
string lockedById = progressBarItem.getProperty("locked_by_id", "");
string currentUserId = innovator.getUserID();
if (lockedById == currentUserId)
{
// Item is already locked by the current user. This is expected in polling.
// Proceed with the update. No error needed.
System.Diagnostics.Trace.WriteLine("DEBUG: Progressbar item (ID: " + progressItemId + ") already locked by current user. Proceeding.");
}
else if (!string.IsNullOrEmpty(lockedById))
{
// Item is locked by another user. This is a genuine conflict.
Item lockedByUser = innovator.getItemById("User", lockedById);
string lockedByUserName = lockedByUser.getProperty("keyed_name", "Unknown User");
return innovator.newError("Failed to lock Progressbar item (ID: " + progressItemId + "): Item is already locked by another user: " + lockedByUserName + ".");
}
else
{
// Item is locked, but locked_by_id is not set or couldn't be determined.
// This might indicate a stale lock or an unexpected state.
return innovator.newError("Failed to lock Progressbar item (ID: " + progressItemId + "): Item is already locked, but the locker could not be identified. Error: " + errorString);
}
}
else
{
// Some other locking error occurred.
return innovator.newError("Failed to lock Progressbar item (ID: " + progressItemId + "): " + errorString);
}
}
// If lockResult is not an error, or if it was already locked by current user,
// the progressBarItem is now locked by this session and ready for update.
// --- Step 3: Simulate work and update progress ---
string currentTaskDescription = "";
bool isComplete = false;
// Simulate different steps
switch (currentStep)
{
case 0:
currentTaskDescription = "Step 1: Preparing data...";
System.Threading.Thread.Sleep(500); // Simulate work
break;
case 1:
currentTaskDescription = "Step 2: Processing records...";
System.Threading.Thread.Sleep(700); // Simulate work
break;
case 2:
currentTaskDescription = "Step 3: Validating inputs...";
System.Threading.Thread.Sleep(400); // Simulate work
break;
case 3:
currentTaskDescription = "Step 4: Performing calculations...";
System.Threading.Thread.Sleep(800); // Simulate work
break;
case 4:
currentTaskDescription = "Step 5: Generating reports...";
System.Threading.Thread.Sleep(600); // Simulate work
break;
case 5:
currentTaskDescription = "Step 6: Saving results...";
System.Threading.Thread.Sleep(500); // Simulate work
break;
case 6:
currentTaskDescription = "Step 7: Finalizing process...";
System.Threading.Thread.Sleep(300); // Simulate work
break;
case 7:
currentTaskDescription = "Step 8: Cleaning up temporary files...";
System.Threading.Thread.Sleep(200); // Simulate work
break;
case 8:
currentTaskDescription = "Step 9: Notifying stakeholders...";
System.Threading.Thread.Sleep(100); // Simulate work
break;
case 9:
currentTaskDescription = "Step 10: Task completed.";
isComplete = true;
break;
default:
currentTaskDescription = "Unknown step or task completed.";
isComplete = true;
break;
}
// Update properties on the progressBarItem object
int barvalue = currentStep + 1; // Increment value for the next step
if (isComplete) barvalue = barmax; // Ensure it reaches max when complete
double barpercent = (double)barvalue / barmax * 100;
progressBarItem.setProperty("barvalue", barvalue.ToString());
progressBarItem.setProperty("barpercent", barpercent.ToString("F2")); // Format to 2 decimal places
progressBarItem.setProperty("currenttask", currentTaskDescription);
progressBarItem.setProperty("debuglog", progressBarItem.getProperty("debuglog", "") + currentTaskDescription + " at " + DateTime.Now.ToString("HH:mm:ss") + "\n");
// --- Step 4: Persist the updated Progressbar item ---
// At this point, progressBarItem *must* be locked by this session.
Item updateResult = progressBarItem.apply("update");
if (updateResult.isError())
{
// If update fails here, it's a critical error.
// This could still be a locking issue if the lock was somehow lost or not acquired correctly.
return innovator.newError("Failed to update Progressbar item (ID: " + progressItemId + "): " + updateResult.getErrorString());
}
// --- Step 5: Prepare the response for the client ---
Item resultItem = innovator.newItem("Progressbar", "get"); // Return the updated Progressbar item
resultItem.setProperty("id", progressItemId); // Ensure the ID is returned
resultItem.setProperty("barmin", progressBarItem.getProperty("barmin"));
resultItem.setProperty("barmax", progressBarItem.getProperty("barmax"));
resultItem.setProperty("barvalue", progressBarItem.getProperty("barvalue"));
resultItem.setProperty("barpercent", progressBarItem.getProperty("barpercent"));
resultItem.setProperty("currenttask", progressBarItem.getProperty("currenttask"));
resultItem.setProperty("debuglog", progressBarItem.getProperty("debuglog"));
resultItem.setProperty("errorlog", progressBarItem.getProperty("errorlog"));
resultItem.setProperty("is_complete", isComplete ? "true" : "false");
resultItem.setProperty("next_step", (currentStep + 1).ToString()); // Tell client the next step to send
// --- Step 6: Unlock the item if the task is complete ---
Item unlockResult = progressBarItem.apply("unlock"); // Unlock the item that was locked
if (unlockResult.isError())
{
// Log the unlock error, but don't fail the entire method as the task is complete.
System.Diagnostics.Trace.WriteLine("ERROR: Failed to unlock Progressbar item (ID: " + progressItemId + "): " + unlockResult.getErrorString());
}
return resultItem;