Hi,
I'm experimenting with the ResourceFieldDescriptor, and i was wondering if there is a way to only show the available resources in that list for a certain time range?
This button is part of the DOM definition as I only want to see this button in certain DOM states & in the script which is triggered from the DOM button I immediately know the id of the DOM where I want to store the selected resources again from my interactive automation (would work with a low code app button as well but is a bit less intuitive)
Do note that you need to set the DOM action to interactive before this is possible
https://docs.dataminer.services/user-guide/Advanced_Modules/DOM/DOM_actions.html
This is also possible to set with the DOM editor script on actions
This is some example code from Empower to select some available resources from a certain resource pool & store them back in DOM
<?xml version="1.0" encoding="utf-8" ?>
<DMSScript options="272" xmlns="http://www.skyline.be/automation">
<Name>ServiceOrderApp_SelectAvailableResources</Name>
<Description />
<Type>Automation</Type>
<Author>SKYLINE2\ThomasGH</Author>
<CheckSets>FALSE</CheckSets>
<Folder>DOM_ServiceOrderApp</Folder>
<Protocols>
</Protocols>
<Memory>
</Memory>
<Parameters>
</Parameters>
<Script>
<Exe id="1" type="csharp">
<Value>
<![CDATA[using Skyline.DataMiner.Automation;
using Skyline.DataMiner.Net.Apps.DataMinerObjectModel;
using Skyline.DataMiner.Net.Apps.DataMinerObjectModel.Actions;
using Skyline.DataMiner.Net.Apps.Sections.Sections;
using Skyline.DataMiner.Net.Messages;
using Skyline.DataMiner.Net.Messages.SLDataGateway;
using Skyline.DataMiner.Net.ResourceManager.Objects;
using Skyline.DataMiner.Net.Sections;
using System;
using System.Collections.Generic;
using System.Linq;
using Skyline.DataMiner.DataMinerSolutions.ProcessAutomation.MessageHandler;
using Skyline.DataMiner.DataMinerSolutions.ProcessAutomation.Common.Objects.Exceptions;
namespace DataMiner_Empower_DOM_2023.ServiceOrderApp_SelectAvailableResources
{
public class Script
{
private const string PoolName = "Server Capacity";
private const string ModuleId = "process_automation";
private const string StartTimeFieldName = "Start time";
private const string EndTimeFieldName = "End time";
private const string SelectedResourcesFieldName = "Selected resources";
private const string OrderIdFieldName = "Order ID";
private const string BookingDetailsSectionDefinitionName = "Booking details";
private const string OrderDetailsSectionDefinitionName = "Order details";
private const string NewToWaitingForApprovalTransitionId = "new_to_waiting_for_approval";
private const string PaProcessName = "Service order";
private static readonly TimeSpan OrderMinimumStartOffset = TimeSpan.FromMinutes(30);
private static readonly TimeSpan OrderMinimumDuration = TimeSpan.FromMinutes(5);
private IEngine _engine;
private DomHelper _domHelper;
private ResourceManagerHelper _rmHelper;
private SectionDefinition _bookingDetailsSectionDefinition;
public void Run(Engine engine)
{
// Not doing anything
}
{
try
{
Setup(engine);
InnerRun(context);
}
catch (Exception ex)
{
FailPopUp(ex.Message);
}
}
private void Setup(IEngine engine)
{
_engine = engine;
_domHelper = new DomHelper(_engine.SendSLNetMessages, ModuleId)
{
SectionDefinitions =
{
ThrowExceptionsOnErrorData = false
},
DomDefinitions = { ThrowExceptionsOnErrorData = false },
DomBehaviorDefinitions = { ThrowExceptionsOnErrorData = false },
DomInstances = { ThrowExceptionsOnErrorData = false },
DomTemplates = { ThrowExceptionsOnErrorData = false },
DomInstanceHistory = { ThrowExceptionsOnErrorData = false }
};
_rmHelper = new ResourceManagerHelper(_engine.SendSLNetSingleResponseMessage);
}
private void InnerRun(ExecuteScriptDomActionContext context)
{
// Check the provided ExecuteScriptDomActionContext
var domInstanceId = ValidateExecuteScriptDomActionContext(context);
if (domInstanceId == null)
{
return;
}
// Retrieve the DomInstance
var domInstance = RetrieveDomInstance(domInstanceId);
if (domInstance == null)
{
FailPopUp($"Retrieving the DomInstance with ID '{domInstanceId}' returned a null object");
return;
}
// Retrieve the SectionDefinition
_bookingDetailsSectionDefinition = GetSectionDefinitionByName(BookingDetailsSectionDefinitionName);
if (_bookingDetailsSectionDefinition == null)
{
FailPopUp($"The SectionDefinition to store the selected resources was not found on the system. (Name: {BookingDetailsSectionDefinitionName})");
return;
}
// Stitch the DomInstance so we have easy access to the configuration objects
LogInformationEvent($"Stitching DomInstance '{domInstance.ID.Id}'");
_domHelper.StitchDomInstances(new List<DomInstance> { domInstance });
// Retrieve start and end time from DomInstance
var startTimeFieldValue = GetFieldValue<DateTime>(domInstance, OrderDetailsSectionDefinitionName, StartTimeFieldName);
if (startTimeFieldValue == null)
{
FailPopUp($"Retrieving value for field '{StartTimeFieldName}' on DomInstance '{domInstance.ID.Id}' returned null");
return;
}
var startTime = startTimeFieldValue.Value;
// Check if the start time is starting at least OrderMinimumStartOffset from now
if (DateTime.UtcNow.Add(OrderMinimumStartOffset) > startTime)
{
FailPopUp($"Please review the start time of your order.{Environment.NewLine}Your order should start in at least {OrderMinimumStartOffset.TotalMinutes} minutes and have a {OrderMinimumDuration.TotalMinutes} minutes minimum duration.");
return;
}
var endTimeFieldValue = GetFieldValue<DateTime>(domInstance, OrderDetailsSectionDefinitionName, EndTimeFieldName);
if (endTimeFieldValue == null)
{
FailPopUp($"Retrieving value for field '{EndTimeFieldName}' on DomInstance '{domInstance.ID.Id}' returned null");
return;
}
var endTime = endTimeFieldValue.Value;
// Check if the start time is not bigger than the end time and has a OrderMinimumDuration
if (startTime.Add(OrderMinimumDuration) > endTime)
{
FailPopUp($"Please review the end time of your order.{Environment.NewLine}Your order should start in at least {OrderMinimumStartOffset.TotalMinutes} minutes and have a {OrderMinimumDuration.TotalMinutes} minutes minimum duration.");
return;
}
// Retrieve eligible resources
var resources = GetEligibleResources(startTime, endTime);
if (resources == null)
{
return;
}
// Let the user select resources
resources = resources.OrderBy(x => x.Name).ToList();
var selectedResources = DrawUi(resources);
if (!selectedResources.Any())
{
FailPopUp("No resources were selected");
return;
}
// Update DomInstance
if (!TryUpdateResourcesOnDomInstance(domInstance, selectedResources))
{
return;
}
// Transition the DomInstance
_domHelper.DomInstances.DoStatusTransition(domInstance.ID, NewToWaitingForApprovalTransitionId);
var traceData = _domHelper.DomInstances.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Failed to transition the DomInstance: {traceData}");
return;
}
// Push a PA token
PushPaToken(domInstance);
}
private DomInstanceId ValidateExecuteScriptDomActionContext(ExecuteScriptDomActionContext context)
{
if (context == null)
{
FailPopUp($"The provided {nameof(ExecuteScriptDomActionContext)} is null");
return null;
}
if (context.ActionId == null)
{
FailPopUp($"Unable to retrieve the 'ActionId' on the {nameof(ExecuteScriptDomActionContext)}");
return null;
}
if (context.ContextId == null)
{
FailPopUp($"Unable to retrieve the 'ContextId' on the {nameof(ExecuteScriptDomActionContext)}");
return null;
}
var domInstanceId = context.ContextId as DomInstanceId;
if (domInstanceId == null)
{
FailPopUp($"The provided 'ContextId' is not a '{nameof(DomInstanceId)}'");
return null;
}
if (domInstanceId.ModuleId != ModuleId)
{
FailPopUp($"The provided DomInstance belongs to module '{domInstanceId.ModuleId}', although it should belong to module '{ModuleId}'");
return null;
}
if (domInstanceId.Id == Guid.Empty)
{
FailPopUp($"The provided DomInstance doesn't have a valid ID: {domInstanceId.Id}");
return null;
}
return domInstanceId;
}
private DomInstance RetrieveDomInstance(DomInstanceId domInstanceId)
{
var domInstance = _domHelper.DomInstances.Read(DomInstanceExposers.Id.Equal(domInstanceId)).FirstOrDefault();
var traceData = _domHelper.DomInstances.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Unable to retrieve DomInstance with ID '{domInstanceId}': {traceData}");
return null;
}
return domInstance;
}
private ValueWrapper<T> GetFieldValue<T>(DomInstance domInstance, string sectionDefinitionName, string fieldName)
{
foreach (var section in domInstance.Sections)
{
var sectionDefinition = section.GetSectionDefinition();
if (sectionDefinition.GetName() != sectionDefinitionName)
{
break;
}
var fields = sectionDefinition.GetAllFieldDescriptors();
foreach (var field in fields)
{
if (field.Name.Equals(fieldName))
{
return domInstance.GetFieldValue<T>(sectionDefinition, field);
}
}
}
return null;
}
private List<Resource> GetEligibleResources(DateTime start, DateTime end)
{
var pool = GetResourcePool(PoolName);
if (pool == null)
{
return null;
}
LogInformationEvent($"Retrieving the eligible resources in the timespan [{start.ToLocalTime()}, {end.ToLocalTime()}]");
var context = new EligibleResourceContext()
{
ResourceFilter = ResourceExposers.PoolGUIDs.Contains(pool.ID),
TimeRange = new Skyline.DataMiner.Net.Time.TimeRangeUtc(start, end)
};
var result = _rmHelper.GetEligibleResources(context);
var traceData = _rmHelper.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Retrieving the eligible resources failed: {traceData}");
return null;
}
if (result == null)
{
FailPopUp($"Retrieving the eligible resources returned a null object");
return null;
}
if (result.EligibleResources.Any())
{
return result.EligibleResources;
}
FailPopUp("There are no eligible resources on the system");
return null;
}
private List<Resource> DrawUi(List<Resource> resources)
{
var uiBuilder = new UIBuilder()
{
Title = "Resource selection",
RowDefs = "a;a;a",
ColumnDefs = "a",
RequireResponse = true
};
var info = new UIBlockDefinition()
{
Type = UIBlockType.StaticText,
ColumnSpan = 1,
Row = 0,
Column = 0,
Text = "Please select the resources you want to book and click the Apply button"
};
uiBuilder.AppendBlock(info);
var list = new UIBlockDefinition()
{
Type = UIBlockType.CheckBoxList,
Row = 1,
Column = 0,
DestVar = "resources"
};
foreach (var resource in resources)
{
list.AddCheckBoxListOption(resource.Name);
}
uiBuilder.AppendBlock(list);
var applyButton = new UIBlockDefinition()
{
Type = UIBlockType.Button,
Row = 2,
Column = 0,
WantsOnChange = true,
Text = "Apply"
};
uiBuilder.AppendBlock(applyButton);
var results = _engine.ShowUI(uiBuilder);
return resources.Where(x => results.GetChecked("resources", x.Name)).ToList();
}
private bool TryUpdateResourcesOnDomInstance(DomInstance domInstance, List<Resource> resources)
{
var fieldDescriptor = _bookingDetailsSectionDefinition.GetAllFieldDescriptors()
.FirstOrDefault(one => one.Name == SelectedResourcesFieldName);
if (fieldDescriptor == null)
{
FailPopUp($"No FieldDescriptor was found with name '{SelectedResourcesFieldName}' " +
$"on the SectionDefinition with ID '{_bookingDetailsSectionDefinition.GetID().Id}'");
return false;
}
domInstance.AddOrUpdateListFieldValue(_bookingDetailsSectionDefinition, fieldDescriptor, resources.Select(x => x.ID).ToList());
_domHelper.DomInstances.Update(domInstance);
var traceData = _domHelper.DomInstances.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Unable to update the '{SelectedResourcesFieldName}' field on DomInstance '{domInstance.ID.Id}': {traceData}");
return false;
}
return true;
}
private ResourcePool GetResourcePool(string poolName)
{
var retrievedPool = _rmHelper.GetResourcePools(new ResourcePool() { Name = poolName }).FirstOrDefault();
var traceData = _rmHelper.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Unable to retrieve pool '{poolName}': {traceData}");
return null;
}
if (retrievedPool == null)
{
FailPopUp($"Retrieving pool with name '{poolName}' returned null");
return null;
}
return retrievedPool;
}
private void FailPopUp(string message)
{
var uiBuilder = new UIBuilder()
{
Title = "Resource selection failed",
RowDefs = "a;a",
ColumnDefs = "a",
RequireResponse = true
};
var info = new UIBlockDefinition()
{
Type = UIBlockType.StaticText,
ColumnSpan = 1,
Row = 0,
Column = 0,
Text = message
};
uiBuilder.AppendBlock(info);
var applyButton = new UIBlockDefinition()
{
Type = UIBlockType.Button,
Row = 2,
Column = 0,
WantsOnChange = true,
Text = "Close"
};
uiBuilder.AppendBlock(applyButton);
_engine.ShowUI(uiBuilder);
}
private void LogInformationEvent(string message)
{
_engine.GenerateInformation(message);
}
private SectionDefinition GetSectionDefinitionByName(string name)
{
var filter = SectionDefinitionExposers.Name.Equal(name);
var sectionDefinition = _domHelper.SectionDefinitions.Read(filter).FirstOrDefault();
var traceData = _domHelper.DomInstances.GetTraceDataLastCall();
if (!traceData.HasSucceeded())
{
FailPopUp($"Retrieving the SectionDefinition by name ({name}) failed with TraceData: {traceData}");
}
return sectionDefinition;
}
private void PushPaToken(DomInstance domInstance)
{
var title = GetFieldValue<string>(domInstance, OrderDetailsSectionDefinitionName, OrderIdFieldName)?.Value ?? $"Unnamed order ({domInstance.ID.Id})";
try
{
ProcessHelper.PushToken(PaProcessName, title, domInstance.ID);
}
catch (PaProcessNotFoundException)
{
// The process does not exist. It is probably not created yet. Ignore this...
_engine.GenerateInformation(
"The resource selection script for the 'Service Order' app tried to push a PA token for the process " +
$"with name '{PaProcessName}' but the process does not exist yet. Continuing execution...");
}
catch (Exception e)
{
FailPopUp($"Pushing the PA token for process {PaProcessName} failed with an exception: {e.Message}");
}
}
}
}
]]>
</Value>
<!--<Param type="debug">true</Param>-->
<Message />
<Param type="ref">C:\Skyline DataMiner\ProtocolScripts\ProcessAutomation.dll</Param>
</Exe>
</Script>
</DMSScript>
Hi Gerwin,
This is not supported. You will have to implement an Interactive automation script and use the GetEligible call. More info about that call here
Hi Gerwen,
In the DOM form itself today we cannot run automation to implement such logic. Therefore it is possible to include interactive automation after a DOM button where automate whatever you want:
Below an example of a production job portal where you fill in start/ end time of the production & then at the bottom you have a select resource button which triggers the interactive automation in the third screenshot. In that interactive automation you only show the available resources in the dropdown which are available in time period you defined in that job hope this helps
Thanks for the example. is that button part of the DOM or just a button on the low code app?