Forum Discussion

bryan-riley's avatar
bryan-riley
Creator II
3 months ago

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;

 

4 Replies

  • Hi Bryan,

    That is a very cool idea, thank you for sharing. Like angela​ said: it has been a while since we saw these hands-on features.

    An idea: maybe you could host an external IOM .NET Core API end-point with streaming (like websockets) that creates a realtime status stream to your HTML page? It might be a more ligthweight solution compared to polling every interval with server methods (and even faster or more real time!).

    On another topic: are you also attending ACE Miami? 

    Thanks again,

    Daan

  • Many thanks for the post! We haven’t had these kinds of experimental projects in this forum for years!

    Here is a collection of random thoughts I have regarding the idea:

    • According to your C# sample, you simulate a long-running C# method. However, I assume it is not actually running asynchronously in the background. Can this work for a ProgressBar for Server Methods?
    • A classic approach for long-running server tasks in Aras Innovator is to use the Conversion Server to execute them in the background.
    • Once I wrote something in this forum suggesting that newer Innovator versions seem to support async server methods. Right now, however, I’m having some trouble finding my previous post and the source that led me to this conclusion… 💀
    • You include extensive error handling - love to see this!'
    • Outdated code: fieldContainerEl = top.document.getElementsByName(propName)[0]; → Remove "top". We no longer use top references in 2026.

     

    Progress bar ideas

    • Idea #1: Use a string property with a percentage value (e.g., "20%"). Color the background according to the percentage. With CSS, you can style the background so that it visually behaves like a growing bar.
    • Idea #2: Use an SSRS or Power BI report with a data bar. If it´s just an admin view that is not needed on a daily basis....
    • Idea #3: Use an Images property. Store many images representing 10%, 20%, … 100% in your code tree. So you just update the image to "update" the progress bar. Customize the stylesheet in the codetree to make this specific image property larger. Yes, I have tried something like this before. A lot of work to get the stylesheet right..... :D. 
    • bryan-riley's avatar
      bryan-riley
      Creator II

      Thanks! I've continued working on this project, and am successfully using it as a progressbar & logging mechanism for one of our server methods.

       I moved the trigger to an action on a part, that javascript method creates the progressbar item and passes any required information (part ID, etc) to the progressbar item, and onload of the form it triggers the second javascript method which 'initializes' the process and then repeatedly calls the server method to do the processing. So a 3-method system... trigger, controller, server

      This appears to have the benefit of giving the server a break so it doesn't bog down, and I introduced a timer to all me to adjust that spacing between calls... and I can also put debugging information into the console if I like

      Using the conversion server is a good idea too, how do you tell Aras what server to process a method on?

      I was also wondering about the Async methods, I'm guessing its there (C# supports it), but I haven't tried it.   but it doesn't solve my logging needs. 

      It also goes very slow if its 1 by 1 part on a large BOM (too much updating) so now it can call the server method in 'chunks' , ex it processes 100 parts in a BOM at a time... but the progressbar is going in chunks too so I'm trying to figure out how to balance the visual with the performance. 

      The CSS idea is fantastic! I'll try it this week and let you know how it goes (I'm not great with the HTML / CSS world). Where did you put the '.state...' code into Aras? Images was my backup plan, I could probably make .jpg of the bar at 1% increments and change it and it would update on a refresh? maybe not that much better than the % number I already have... 

       

    • angela's avatar
      angela
      Catalyst II

      Quick test with using custom CSS in a random "state" property:

      .state{background: linear-gradient(to right,#4caf50 0%,#4caf50 20%,#ddd 20%,#ddd 100% );}