mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-26 07:00:17 -05:00
Workflows: Add interactive editor for taskmaestro workflows
Build, copy and edit taskmaestro workflows in ResInsight and save them as standard workflow.yaml + input.yaml. - Python helper: catalog, load, save and export-registered subcommands, and run --registered for installed workflows - C++ definition model with pure edit operations, port type checks, config field derivation, static validation and undo/redo - Installed folder listing registered workflows (read-only, runnable, can be duplicated) - Graph editor dock with task palette, drag-to-connect, context menus, automatic layout and debounced live validation - New/Duplicate/Save/Save As/Rescan commands; unsaved edits run from a temporary folder and prompt before being discarded - Update the design document and the user README
This commit is contained in:
1 parent
1342e0987a
commit
e6dc62a160
120 files changed
+12637
-678
No files matched your search
@@ -1,7 +1,15 @@
|
||||
set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflow.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowCollection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowValidationTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowInstalledCollection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDescribeTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowSchemaTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDefinition.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDefinitionTools.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowPortCompatibility.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowHelperProcess.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowTaskCatalog.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowJob.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowTaskInput.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFieldBinding.cpp
|
||||
@@ -16,6 +24,7 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowDateBinding.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowFilePathBinding.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowVec3Binding.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RimWorkflowMappingBinding.cpp
|
||||
)
|
||||
|
||||
list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES})
|
||||
|
||||
@@ -15,30 +15,37 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflow.h"
|
||||
|
||||
#include "RimWorkflowDefinitionTools.h"
|
||||
#include "RimWorkflowDescribeTools.h"
|
||||
#include "RimWorkflowHelperProcess.h"
|
||||
#include "RimWorkflowJob.h"
|
||||
#include "RimWorkflowTaskInput.h"
|
||||
#include "RimWorkflowValidationTools.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaPreferences.h"
|
||||
#include "RiaQStringFormatter.h"
|
||||
|
||||
#include "RiuMainWindow.h"
|
||||
|
||||
#include "cafCmdFeatureMenuBuilder.h"
|
||||
#include "cafPdmUiOrdering.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflow, "Workflow" );
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr size_t maxUndoSteps = 100;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -55,12 +62,26 @@ RimWorkflow::RimWorkflow()
|
||||
CAF_PDM_InitFieldNoDefault( &m_workflowDirectory, "WorkflowDirectory", "Directory" );
|
||||
m_workflowDirectory.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_registeredId, "RegisteredId", "Registered Id" );
|
||||
m_registeredId.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitField( &m_editable, "Editable", false, "Editable" );
|
||||
m_editable.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_readOnlyReason, "ReadOnlyReason", "Read-only Because" );
|
||||
m_readOnlyReason.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_loadError, "LoadError", "Load Error" );
|
||||
m_loadError.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_jobs, "Jobs", "" );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflow::~RimWorkflow() = default;
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -75,9 +96,9 @@ QString RimWorkflow::name() const
|
||||
void RimWorkflow::setWorkflowDirectory( const QString& directory )
|
||||
{
|
||||
m_workflowDirectory = directory;
|
||||
QString name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
|
||||
m_name = name;
|
||||
setUiName( name );
|
||||
m_sourceDirectory = directory;
|
||||
m_name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
|
||||
updateUiName();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -111,9 +132,9 @@ std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
|
||||
{
|
||||
std::vector<RimWorkflowJob*> result;
|
||||
result.reserve( m_jobs.size() );
|
||||
for ( RimWorkflowJob* j : m_jobs.childrenByType() )
|
||||
for ( RimWorkflowJob* job : m_jobs.childrenByType() )
|
||||
{
|
||||
if ( j ) result.push_back( j );
|
||||
if ( job ) result.push_back( job );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -123,7 +144,19 @@ std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::addJob( RimWorkflowJob* job )
|
||||
{
|
||||
if ( job ) m_jobs.push_back( job );
|
||||
if ( !job ) return;
|
||||
m_jobs.push_back( job );
|
||||
job->syncTaskInputs( m_graph.value( "tasks" ).toArray() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::syncJobs( const QMap<QString, QString>& renames )
|
||||
{
|
||||
const QJsonArray tasks = m_graph.value( "tasks" ).toArray();
|
||||
for ( RimWorkflowJob* job : jobs() )
|
||||
job->syncTaskInputs( tasks, renames );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -132,115 +165,671 @@ void RimWorkflow::addJob( RimWorkflowJob* job )
|
||||
void RimWorkflow::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
|
||||
{
|
||||
menuBuilder << "RicNewWorkflowJobFeature";
|
||||
menuBuilder << "Separator";
|
||||
menuBuilder << "RicSaveWorkflowFeature";
|
||||
menuBuilder << "RicSaveWorkflowAsFeature";
|
||||
menuBuilder << "RicDuplicateWorkflowAsEditableFeature";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
|
||||
{
|
||||
m_name.uiCapability()->setUiReadOnly( !isEditable() || isLocked() );
|
||||
|
||||
uiOrdering.add( &m_name );
|
||||
uiOrdering.add( &m_description );
|
||||
if ( m_source == Source::Registered )
|
||||
uiOrdering.add( &m_registeredId );
|
||||
else if ( !workflowDirectory().isEmpty() )
|
||||
uiOrdering.add( &m_workflowDirectory );
|
||||
uiOrdering.add( &m_editable );
|
||||
if ( !m_readOnlyReason().isEmpty() ) uiOrdering.add( &m_readOnlyReason );
|
||||
if ( !m_loadError().isEmpty() ) uiOrdering.add( &m_loadError );
|
||||
uiOrdering.skipRemainingFields( true );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
|
||||
{
|
||||
if ( changedField != &m_name ) return;
|
||||
|
||||
auto result = applyEdit( RimWorkflowDefinitionTools::renameWorkflow( m_definition, m_name() ) );
|
||||
if ( !result )
|
||||
{
|
||||
RiaLogging::warning( result.error().toStdString() );
|
||||
m_name = m_definition.name;
|
||||
updateUiName();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::setLoadError( const QString& message, QString* errorMessage )
|
||||
{
|
||||
m_loadError = message;
|
||||
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( m_name(), message ).toStdString() );
|
||||
if ( errorMessage ) *errorMessage = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Load a YAML folder through `taskmaestro_helper load --describe`
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::loadFromDirectory( QString* errorMessage )
|
||||
{
|
||||
m_jobs.deleteChildren();
|
||||
m_graph = {};
|
||||
m_loadError = "";
|
||||
|
||||
const QString dir = workflowDirectory();
|
||||
if ( dir.isEmpty() ) return false;
|
||||
const QString directory = workflowDirectory();
|
||||
if ( directory.isEmpty() ) return false;
|
||||
if ( !QDir( directory ).exists( "workflow.yaml" ) ) return setLoadError( "Missing workflow.yaml", errorMessage );
|
||||
|
||||
QString python = RimWorkflow::findPythonExecutable();
|
||||
if ( python.isEmpty() )
|
||||
auto output = RimWorkflowHelperProcess::runSync( { "load", QDir( directory ).absolutePath(), "--describe" } );
|
||||
if ( !output ) return setLoadError( output.error(), errorMessage );
|
||||
|
||||
auto definition = RimWorkflowDefinitionTools::fromJson( output.value() );
|
||||
if ( !definition ) return setLoadError( definition.error(), errorMessage );
|
||||
|
||||
m_registeredId = "";
|
||||
setDefinition( definition.value(), Source::YamlDirectory );
|
||||
recordModificationTime();
|
||||
|
||||
const auto validation =
|
||||
RimWorkflowValidationTools::issuesFromDescribe( m_definition.describe, m_definition.describeError, m_definition.nodeNames() );
|
||||
setValidationResult( m_revision, validation.issues, validation.describeSucceeded );
|
||||
if ( !validation.describeSucceeded )
|
||||
{
|
||||
m_loadError = "No usable Python interpreter found";
|
||||
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
m_loadError = RimWorkflowHelperProcess::errorMessage( m_definition.describeError );
|
||||
RiaLogging::warning( QString( "Workflow '%1' is not valid: %2" ).arg( m_name(), m_loadError() ).toStdString() );
|
||||
}
|
||||
|
||||
const QDir workflowDir( dir );
|
||||
if ( !workflowDir.exists( "workflow.yaml" ) )
|
||||
RiaLogging::info(
|
||||
QString( "Loaded workflow '%1' (%2 tasks) from %3" ).arg( m_name() ).arg( m_definition.nodes.size() ).arg( directory ).toStdString() );
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A workflow registered by an installed package, as listed by `taskmaestro_helper catalog`.
|
||||
/// Registered workflows are read-only; Duplicate as Editable makes an editable copy.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::loadFromRegistered( const QJsonObject& catalogEntry, QString* errorMessage )
|
||||
{
|
||||
m_graph = {};
|
||||
m_loadError = "";
|
||||
m_registeredId = catalogEntry.value( "id" ).toString();
|
||||
m_name = catalogEntry.value( "name" ).toString( m_registeredId() );
|
||||
m_sourceDirectory.clear();
|
||||
|
||||
RimWorkflowDefinition definition;
|
||||
if ( catalogEntry.value( "definition" ).isObject() )
|
||||
{
|
||||
m_loadError = "Missing workflow.yaml";
|
||||
RiaLogging::warning( std::format( "Workflow '{}': {}", dir, m_loadError() ) );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
auto parsed = RimWorkflowDefinitionTools::fromJson( catalogEntry.value( "definition" ).toObject() );
|
||||
if ( !parsed ) return setLoadError( parsed.error(), errorMessage );
|
||||
definition = parsed.value();
|
||||
}
|
||||
else
|
||||
{
|
||||
definition.name = m_name();
|
||||
definition.editable = false;
|
||||
}
|
||||
|
||||
QStringList args{ "-m", "taskmaestro", "workflow", "describe", workflowDir.absoluteFilePath( "workflow.yaml" ), "--json" };
|
||||
if ( workflowDir.exists( "input.yaml" ) ) args << "--input" << workflowDir.absoluteFilePath( "input.yaml" );
|
||||
|
||||
QProcess proc;
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
proc.setProcessEnvironment( env );
|
||||
proc.start( python, args );
|
||||
if ( !proc.waitForStarted( 10000 ) )
|
||||
// `definition.editable` tells whether an editable copy is possible; the installed workflow
|
||||
// itself is always read-only (see isEditable())
|
||||
for ( const QJsonValue& reason : catalogEntry.value( "readonly_reasons" ).toArray() )
|
||||
{
|
||||
m_loadError = QString( "Could not launch '%1'" ).arg( python );
|
||||
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
}
|
||||
if ( !proc.waitForFinished( 30000 ) )
|
||||
{
|
||||
proc.kill();
|
||||
m_loadError = "taskmaestro workflow describe timed out";
|
||||
RiaLogging::warning( QString( "Workflow '%1': %2" ).arg( dir, m_loadError() ).toStdString() );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
if ( !definition.readOnlyReasons.contains( reason.toString() ) ) definition.readOnlyReasons.append( reason.toString() );
|
||||
}
|
||||
if ( !definition.readOnlyReasons.isEmpty() ) definition.editable = false;
|
||||
if ( definition.editable ) definition.readOnlyReasons.append( "Installed workflows are read-only; duplicate to edit" );
|
||||
if ( definition.describe.isEmpty() ) definition.describe = catalogEntry.value( "describe" ).toObject();
|
||||
|
||||
const QByteArray stdoutBytes = proc.readAllStandardOutput();
|
||||
QJsonParseError parseErr{};
|
||||
const QJsonDocument doc = QJsonDocument::fromJson( stdoutBytes, &parseErr );
|
||||
setDefinition( definition, Source::Registered );
|
||||
if ( m_graph.value( "tasks" ).toArray().isEmpty() ) return setLoadError( "The workflow could not be described", errorMessage );
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0 )
|
||||
{
|
||||
// In JSON mode, configuration errors are reported on stdout; other errors on stderr
|
||||
m_loadError = RimWorkflowDescribeTools::errorFromDescribe( doc.object() );
|
||||
if ( m_loadError().isEmpty() ) m_loadError = QString::fromUtf8( proc.readAllStandardError() ).trimmed();
|
||||
if ( m_loadError().isEmpty() ) m_loadError = "taskmaestro workflow describe failed";
|
||||
RiaLogging::warning( std::format( "Workflow '{}' describe failed: {}", dir, m_loadError() ) );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
}
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A new workflow, or an editable copy. `sourceDirectory` holds Python modules used by the tasks,
|
||||
/// and `targetDirectory` is the folder the first Save writes to.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::setUnsavedDefinition( const RimWorkflowDefinition& definition, const QString& sourceDirectory, const QString& targetDirectory )
|
||||
{
|
||||
m_workflowDirectory = targetDirectory;
|
||||
m_registeredId = "";
|
||||
m_loadError = "";
|
||||
m_sourceDirectory = sourceDirectory;
|
||||
|
||||
if ( parseErr.error != QJsonParseError::NoError || !doc.isObject() )
|
||||
{
|
||||
m_loadError = QString( "Invalid JSON from taskmaestro workflow describe: %1" ).arg( parseErr.errorString() );
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
}
|
||||
RimWorkflowDefinition editable = definition;
|
||||
if ( definition.editable ) editable.readOnlyReasons.clear();
|
||||
editable.readOnlyReasons.append( RimWorkflowDefinitionTools::structuralReadOnlyReasons( definition ) );
|
||||
editable.readOnlyReasons.removeDuplicates();
|
||||
editable.editable = editable.readOnlyReasons.isEmpty();
|
||||
setDefinition( editable, Source::Unsaved );
|
||||
}
|
||||
|
||||
if ( !doc.object().value( "tasks" ).isArray() )
|
||||
{
|
||||
m_loadError = "Invalid workflow graph schema";
|
||||
if ( errorMessage ) *errorMessage = m_loadError;
|
||||
return false;
|
||||
}
|
||||
const QJsonObject root = RimWorkflowDescribeTools::graphFromDescribe( doc.object() );
|
||||
m_graph = root;
|
||||
if ( root.contains( "name" ) ) m_name = root.value( "name" ).toString();
|
||||
if ( root.contains( "description" ) ) m_description = root.value( "description" ).toString();
|
||||
setUiName( m_name() );
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::setDefinition( const RimWorkflowDefinition& definition, Source source )
|
||||
{
|
||||
m_source = source;
|
||||
m_definition = definition;
|
||||
m_savedDefinition = definition;
|
||||
m_undoStack.clear();
|
||||
m_redoStack.clear();
|
||||
m_validationIssues.clear();
|
||||
m_validationRevision = -1;
|
||||
++m_revision;
|
||||
|
||||
std::vector<RimWorkflowTaskInput*> taskInputs;
|
||||
for ( const QJsonValue& tv : root.value( "tasks" ).toArray() )
|
||||
if ( !definition.name.isEmpty() ) m_name = definition.name;
|
||||
m_description = definition.headerComment;
|
||||
m_editable = isEditable();
|
||||
m_readOnlyReason = definition.readOnlyReasons.join( "\n" );
|
||||
|
||||
rebuildGraph();
|
||||
ensureDefaultJob();
|
||||
syncJobs();
|
||||
updateUiName();
|
||||
notifyDefinitionChanged();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflow::Source RimWorkflow::source() const
|
||||
{
|
||||
return m_source;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflow::registeredId() const
|
||||
{
|
||||
return m_registeredId();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The folder with the Python modules of the workflow (the YAML folder or the folder it was copied from)
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflow::sourceDirectory() const
|
||||
{
|
||||
return m_sourceDirectory;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
const RimWorkflowDefinition& RimWorkflow::definition() const
|
||||
{
|
||||
return m_definition;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::isEditable() const
|
||||
{
|
||||
return m_definition.editable && m_source != Source::Registered;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflow::readOnlyReason() const
|
||||
{
|
||||
return m_readOnlyReason();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::isDirty() const
|
||||
{
|
||||
return m_source == Source::Unsaved || m_definition != m_savedDefinition;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The workflow cannot be edited while one of its jobs is running
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::isLocked() const
|
||||
{
|
||||
for ( RimWorkflowJob* job : jobs() )
|
||||
{
|
||||
const QJsonObject taskObj = tv.toObject();
|
||||
if ( taskObj.value( "config_fields" ).toArray().isEmpty() ) continue;
|
||||
auto* input = new RimWorkflowTaskInput;
|
||||
input->setTaskName( taskObj.value( "name" ).toString() );
|
||||
input->buildFromSchema( taskObj.value( "config_fields" ).toArray() );
|
||||
taskInputs.push_back( input );
|
||||
if ( job->isRunning() ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int RimWorkflow::revision() const
|
||||
{
|
||||
return m_revision;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Apply the result of an edit operation from RimWorkflowDefinitionTools. `renames` maps old task
|
||||
/// names to new names, so that input values follow renamed tasks.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflow::applyEdit( const std::expected<RimWorkflowDefinition, QString>& result,
|
||||
const QMap<QString, QString>& renames )
|
||||
{
|
||||
if ( !result ) return std::unexpected( result.error() );
|
||||
if ( !isEditable() )
|
||||
return std::unexpected( m_readOnlyReason().isEmpty() ? QString( "The workflow is read-only" ) : m_readOnlyReason() );
|
||||
if ( isLocked() ) return std::unexpected( "The workflow cannot be edited while a job is running" );
|
||||
if ( result.value() == m_definition ) return {};
|
||||
|
||||
m_undoStack.push_back( { m_definition, renames } );
|
||||
if ( m_undoStack.size() > maxUndoSteps ) m_undoStack.erase( m_undoStack.begin() );
|
||||
m_redoStack.clear();
|
||||
|
||||
m_definition = result.value();
|
||||
definitionChanged( renames );
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::canUndo() const
|
||||
{
|
||||
return isEditable() && !isLocked() && !m_undoStack.empty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::canRedo() const
|
||||
{
|
||||
return isEditable() && !isLocked() && !m_redoStack.empty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QMap<QString, QString> RimWorkflow::undoRenames() const
|
||||
{
|
||||
return m_undoStack.empty() ? QMap<QString, QString>() : invertedRenames( m_undoStack.back().renames );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QMap<QString, QString> RimWorkflow::redoRenames() const
|
||||
{
|
||||
return m_redoStack.empty() ? QMap<QString, QString>() : m_redoStack.back().renames;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::undo()
|
||||
{
|
||||
if ( !canUndo() ) return;
|
||||
|
||||
UndoEntry entry = m_undoStack.back();
|
||||
m_undoStack.pop_back();
|
||||
m_redoStack.push_back( { m_definition, entry.renames } );
|
||||
m_definition = entry.definition;
|
||||
definitionChanged( invertedRenames( entry.renames ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::redo()
|
||||
{
|
||||
if ( !canRedo() ) return;
|
||||
|
||||
UndoEntry entry = m_redoStack.back();
|
||||
m_redoStack.pop_back();
|
||||
m_undoStack.push_back( { m_definition, entry.renames } );
|
||||
m_definition = entry.definition;
|
||||
definitionChanged( entry.renames );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::definitionChanged( const QMap<QString, QString>& renames )
|
||||
{
|
||||
++m_revision;
|
||||
m_name = m_definition.name;
|
||||
m_description = m_definition.headerComment;
|
||||
|
||||
// Keep the issues that have not been checked again until the next validation
|
||||
rebuildGraph();
|
||||
syncJobs( renames );
|
||||
updateUiName();
|
||||
notifyDefinitionChanged();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Let the workflow editor show the new state
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::notifyDefinitionChanged()
|
||||
{
|
||||
if ( auto* window = RiuMainWindow::instance() ) window->workflowDefinitionChanged( this );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QMap<QString, QString> RimWorkflow::invertedRenames( const QMap<QString, QString>& renames )
|
||||
{
|
||||
QMap<QString, QString> inverted;
|
||||
for ( auto it = renames.begin(); it != renames.end(); ++it )
|
||||
inverted.insert( it.value(), it.key() );
|
||||
return inverted;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::rebuildGraph()
|
||||
{
|
||||
if ( isEditable() )
|
||||
{
|
||||
std::vector<RimWorkflowIssue> describeIssues;
|
||||
if ( isValidationCurrent() ) describeIssues = m_validationIssues;
|
||||
m_graph = RimWorkflowDefinitionTools::graphFromDefinition( m_definition, true, describeIssues );
|
||||
}
|
||||
else if ( !m_definition.describe.isEmpty() )
|
||||
{
|
||||
m_graph = RimWorkflowDescribeTools::graphFromDescribe( m_definition.describe );
|
||||
m_graph["editable"] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_graph = RimWorkflowDefinitionTools::graphFromDefinition( m_definition, false );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::updateUiName()
|
||||
{
|
||||
setUiName( isDirty() && isEditable() ? m_name() + " *" : m_name() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::ensureDefaultJob()
|
||||
{
|
||||
if ( !m_jobs.empty() ) return;
|
||||
|
||||
auto* job = new RimWorkflowJob;
|
||||
job->setJobName( "Default" );
|
||||
job->setTaskInputs( taskInputs );
|
||||
m_jobs.push_back( job );
|
||||
}
|
||||
|
||||
RiaLogging::info(
|
||||
QString( "Loaded workflow '%1' (%2 tasks with config) from %3" ).arg( m_name() ).arg( taskInputs.size() ).arg( dir ).toStdString() );
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::setValidationResult( int revision, const std::vector<RimWorkflowIssue>& issues, bool describeSucceeded )
|
||||
{
|
||||
if ( revision != m_revision ) return;
|
||||
|
||||
return true;
|
||||
m_validationIssues = issues;
|
||||
m_validationRevision = revision;
|
||||
m_describeSucceeded = describeSucceeded;
|
||||
rebuildGraph();
|
||||
notifyDefinitionChanged();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflowIssue> RimWorkflow::validationIssues() const
|
||||
{
|
||||
return m_validationIssues;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::isValidationCurrent() const
|
||||
{
|
||||
return m_validationRevision == m_revision;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// True when the current revision has errors, from the static checks or from the last describe
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflow::lastValidationFailed() const
|
||||
{
|
||||
if ( isValidationCurrent() && !m_describeSucceeded ) return true;
|
||||
if ( !isEditable() ) return false;
|
||||
for ( const auto& issue : RimWorkflowDefinitionTools::validate( m_definition ) )
|
||||
{
|
||||
if ( issue.severity == RimWorkflowIssue::Severity::Error ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// input.yaml content written on save: the literal values of the first job, falling back to the
|
||||
/// values loaded from the file for tasks that have no inputs in the job
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflow::inputValuesForSave() const
|
||||
{
|
||||
QJsonObject inputs = m_definition.inputs;
|
||||
const auto allJobs = jobs();
|
||||
if ( allJobs.empty() ) return inputs;
|
||||
|
||||
const QJsonObject literal = allJobs.front()->literalInputValues();
|
||||
for ( const auto& node : m_definition.nodes )
|
||||
{
|
||||
if ( literal.contains( node.name ) ) inputs[node.name] = literal.value( node.name );
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QJsonObject, QString> RimWorkflow::saveToDirectory( const QString& directory, bool backup )
|
||||
{
|
||||
QStringList args{ "save", "--out-dir", directory, "--describe" };
|
||||
if ( backup ) args << "--backup";
|
||||
|
||||
auto output =
|
||||
RimWorkflowHelperProcess::runSync( args, definitionJsonForSave(), RimWorkflowHelperProcess::defaultTimeoutMs, m_sourceDirectory );
|
||||
if ( !output ) return output;
|
||||
|
||||
m_definition.inputs = inputValuesForSave();
|
||||
m_definition.describe = output->value( "describe" ).toObject();
|
||||
m_definition.describeError = output->value( "describe_error" ).toObject();
|
||||
m_definition.source = QJsonObject{ { "workflow_yaml", output->value( "workflow_yaml" ) },
|
||||
{ "input_yaml", output->value( "input_yaml" ) },
|
||||
{ "registered_id", QJsonValue::Null } };
|
||||
m_savedDefinition = m_definition;
|
||||
m_source = Source::YamlDirectory;
|
||||
m_loadedModificationTime = QFileInfo( QDir( directory ).absoluteFilePath( "workflow.yaml" ) ).lastModified();
|
||||
|
||||
const auto validation = RimWorkflowValidationTools::issuesFromHelperResult( output.value(), m_definition.nodeNames() );
|
||||
setValidationResult( m_revision, validation.issues, validation.describeSucceeded );
|
||||
updateUiName();
|
||||
return output;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflow::recordModificationTime()
|
||||
{
|
||||
m_loadedModificationTime = QFileInfo( QDir( workflowDirectory() ).absoluteFilePath( "workflow.yaml" ) ).lastModified();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Reasons to think twice before overwriting the workflow folder: workflow.yaml was changed by
|
||||
/// someone else since it was loaded, or the folder is a link into a git checkout
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflow::saveInPlaceWarnings() const
|
||||
{
|
||||
QStringList warnings;
|
||||
if ( m_source != Source::YamlDirectory || workflowDirectory().isEmpty() ) return warnings;
|
||||
|
||||
const QFileInfo yamlFile( QDir( workflowDirectory() ).absoluteFilePath( "workflow.yaml" ) );
|
||||
if ( m_loadedModificationTime.isValid() && yamlFile.exists() && yamlFile.lastModified() != m_loadedModificationTime )
|
||||
{
|
||||
warnings << "workflow.yaml has been changed on disk since it was loaded. Saving overwrites those changes.";
|
||||
}
|
||||
|
||||
const QFileInfo directoryInfo( workflowDirectory() );
|
||||
if ( directoryInfo.isSymLink() || yamlFile.isSymLink() )
|
||||
{
|
||||
QDir dir( yamlFile.canonicalPath() );
|
||||
do
|
||||
{
|
||||
if ( dir.exists( ".git" ) )
|
||||
{
|
||||
warnings
|
||||
<< QString( "The workflow folder links into the git checkout '%1'. Saving changes files there." ).arg( dir.absolutePath() );
|
||||
break;
|
||||
}
|
||||
} while ( dir.cdUp() );
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Write workflow.yaml and input.yaml to the workflow folder. A backup is made the first time.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflow::save()
|
||||
{
|
||||
if ( !isEditable() ) return std::unexpected( "The workflow is read-only" );
|
||||
if ( workflowDirectory().isEmpty() ) return std::unexpected( "The workflow has no folder; use Save As" );
|
||||
if ( m_source == Source::Unsaved ) return saveAs( workflowDirectory() );
|
||||
|
||||
auto output = saveToDirectory( workflowDirectory(), true );
|
||||
if ( !output ) return std::unexpected( output.error() );
|
||||
|
||||
RiaLogging::info( QString( "Saved workflow '%1' to %2" ).arg( m_name(), workflowDirectory() ).toStdString() );
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Save to a new folder. Python modules next to the original workflow.yaml are copied along.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflow::saveAs( const QString& directory )
|
||||
{
|
||||
if ( !isEditable() ) return std::unexpected( "The workflow is read-only" );
|
||||
if ( directory.isEmpty() ) return std::unexpected( "No folder given" );
|
||||
|
||||
QDir target( directory );
|
||||
if ( !target.exists() && !QDir().mkpath( directory ) )
|
||||
return std::unexpected( QString( "Could not create the folder '%1'" ).arg( directory ) );
|
||||
|
||||
if ( !m_sourceDirectory.isEmpty() && QDir( m_sourceDirectory ).absolutePath() != target.absolutePath() )
|
||||
{
|
||||
if ( auto copied = copyPythonModules( m_sourceDirectory, target.absolutePath() ); !copied )
|
||||
return std::unexpected( QString( "The workflow was not saved: %1" ).arg( copied.error() ) );
|
||||
}
|
||||
|
||||
auto output = saveToDirectory( target.absolutePath(), false );
|
||||
if ( !output ) return std::unexpected( output.error() );
|
||||
|
||||
m_workflowDirectory = target.absolutePath();
|
||||
m_sourceDirectory = target.absolutePath();
|
||||
m_registeredId = "";
|
||||
updateUiName();
|
||||
notifyDefinitionChanged();
|
||||
|
||||
RiaLogging::info( QString( "Saved workflow '%1' to %2" ).arg( m_name(), target.absolutePath() ).toStdString() );
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The temporary folder that live validation writes the YAML of unsaved edits to. Runs use their
|
||||
/// own folder (see prepareRunSource()), so validation never rewrites a file a run is reading.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QString, QString> RimWorkflow::editDirectory()
|
||||
{
|
||||
if ( !m_editDirectory )
|
||||
{
|
||||
m_editDirectory = std::make_unique<QTemporaryDir>( QDir::tempPath() + "/resinsight_workflow_edit_XXXXXX" );
|
||||
if ( !m_editDirectory->isValid() )
|
||||
{
|
||||
m_editDirectory.reset();
|
||||
return std::unexpected( "Could not create a temporary folder for the workflow" );
|
||||
}
|
||||
}
|
||||
return m_editDirectory->path();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The definition as sent to `taskmaestro_helper save`
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QByteArray RimWorkflow::definitionJsonForSave() const
|
||||
{
|
||||
RimWorkflowDefinition toSave = m_definition;
|
||||
toSave.inputs = inputValuesForSave();
|
||||
return QJsonDocument( RimWorkflowDefinitionTools::toJson( toSave ) ).toJson( QJsonDocument::Compact );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The arguments of `taskmaestro_helper run` for the current state of the workflow. Unsaved edits
|
||||
/// are written as workflow.yaml to `runDirectory`, a folder private to the run.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<RimWorkflow::RunSource, QString> RimWorkflow::prepareRunSource( const QString& runDirectory )
|
||||
{
|
||||
if ( m_source == Source::Registered )
|
||||
{
|
||||
if ( m_registeredId().isEmpty() ) return std::unexpected( "The workflow has no registered id" );
|
||||
return RunSource{ .baseArgs = { "run", "--registered", m_registeredId() } };
|
||||
}
|
||||
|
||||
if ( m_source == Source::YamlDirectory && !isDirty() )
|
||||
{
|
||||
if ( workflowDirectory().isEmpty() ) return std::unexpected( "The workflow has no folder" );
|
||||
return RunSource{ .baseArgs = { "run", workflowDirectory() }, .extraPythonPath = workflowDirectory() };
|
||||
}
|
||||
|
||||
if ( runDirectory.isEmpty() ) return std::unexpected( "No folder to write the edited workflow to" );
|
||||
auto output = RimWorkflowHelperProcess::runSync( { "save", "--out-dir", runDirectory, "--workflow-only" },
|
||||
definitionJsonForSave(),
|
||||
RimWorkflowHelperProcess::defaultTimeoutMs,
|
||||
m_sourceDirectory );
|
||||
if ( !output ) return std::unexpected( output.error() );
|
||||
return RunSource{ .baseArgs = { "run", runDirectory }, .extraPythonPath = m_sourceDirectory };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Copies the `*.py` files and the package folders (with `__init__.py`), without `__pycache__`.
|
||||
/// Files that already exist in the target are kept. Fails on the first file that cannot be copied.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflow::copyPythonModules( const QString& sourceDirectory, const QString& targetDirectory )
|
||||
{
|
||||
const QDir source( sourceDirectory );
|
||||
QDir target( targetDirectory );
|
||||
if ( !target.exists() && !QDir().mkpath( targetDirectory ) )
|
||||
return std::unexpected( QString( "Could not create the folder '%1'" ).arg( targetDirectory ) );
|
||||
|
||||
for ( const QString& fileName : source.entryList( { "*.py" }, QDir::Files ) )
|
||||
{
|
||||
if ( target.exists( fileName ) ) continue;
|
||||
if ( !QFile::copy( source.absoluteFilePath( fileName ), target.absoluteFilePath( fileName ) ) )
|
||||
return std::unexpected( QString( "Could not copy '%1' to '%2'" ).arg( source.absoluteFilePath( fileName ), targetDirectory ) );
|
||||
}
|
||||
|
||||
for ( const QString& folderName : source.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
|
||||
{
|
||||
if ( folderName == "__pycache__" || !QFileInfo::exists( source.absoluteFilePath( folderName + "/__init__.py" ) ) ) continue;
|
||||
if ( auto copied = copyPythonModules( source.absoluteFilePath( folderName ), target.absoluteFilePath( folderName ) ); !copied )
|
||||
return copied;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -256,15 +845,15 @@ QString RimWorkflow::findPythonExecutable()
|
||||
}
|
||||
candidates << "python3" << "python";
|
||||
|
||||
for ( const QString& cand : candidates )
|
||||
for ( const QString& candidate : candidates )
|
||||
{
|
||||
if ( cand.contains( '/' ) || cand.contains( '\\' ) )
|
||||
if ( candidate.contains( '/' ) || candidate.contains( '\\' ) )
|
||||
{
|
||||
if ( QFileInfo( cand ).isExecutable() ) return cand;
|
||||
if ( QFileInfo( candidate ).isExecutable() ) return candidate;
|
||||
}
|
||||
else if ( !QStandardPaths::findExecutable( cand ).isEmpty() )
|
||||
else if ( !QStandardPaths::findExecutable( candidate ).isEmpty() )
|
||||
{
|
||||
return cand;
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -15,24 +15,51 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
|
||||
#include "cafFilePath.h"
|
||||
#include "cafPdmChildArrayField.h"
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
|
||||
#include <expected>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class RimWorkflowJob;
|
||||
class QTemporaryDir;
|
||||
|
||||
//==================================================================================================
|
||||
/// A taskmaestro workflow: a YAML folder, a workflow registered by an installed package, or a new
|
||||
/// workflow that has not been saved yet. Editable workflows keep their definition in memory with
|
||||
/// undo/redo until saved.
|
||||
//==================================================================================================
|
||||
class RimWorkflow : public caf::PdmObject
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
enum class Source
|
||||
{
|
||||
YamlDirectory,
|
||||
Registered,
|
||||
Unsaved
|
||||
};
|
||||
|
||||
struct RunSource
|
||||
{
|
||||
QStringList baseArgs{};
|
||||
QString extraPythonPath{};
|
||||
};
|
||||
|
||||
RimWorkflow();
|
||||
~RimWorkflow() override;
|
||||
|
||||
QString name() const;
|
||||
void setWorkflowDirectory( const QString& directory );
|
||||
@@ -41,20 +68,103 @@ public:
|
||||
QString loadError() const;
|
||||
|
||||
bool loadFromDirectory( QString* errorMessage = nullptr );
|
||||
bool loadFromRegistered( const QJsonObject& catalogEntry, QString* errorMessage = nullptr );
|
||||
void setUnsavedDefinition( const RimWorkflowDefinition& definition,
|
||||
const QString& sourceDirectory = {},
|
||||
const QString& targetDirectory = {} );
|
||||
|
||||
Source source() const;
|
||||
QString registeredId() const;
|
||||
QString sourceDirectory() const;
|
||||
const RimWorkflowDefinition& definition() const;
|
||||
bool isEditable() const;
|
||||
QString readOnlyReason() const;
|
||||
bool isDirty() const;
|
||||
bool isLocked() const;
|
||||
int revision() const;
|
||||
|
||||
// Editing
|
||||
std::expected<void, QString> applyEdit( const std::expected<RimWorkflowDefinition, QString>& result,
|
||||
const QMap<QString, QString>& renames = {} );
|
||||
bool canUndo() const;
|
||||
bool canRedo() const;
|
||||
|
||||
// Task renames (old name -> new name) that undo() and redo() would apply
|
||||
QMap<QString, QString> undoRenames() const;
|
||||
QMap<QString, QString> redoRenames() const;
|
||||
void undo();
|
||||
void redo();
|
||||
|
||||
// Saving and running
|
||||
std::expected<void, QString> save();
|
||||
QStringList saveInPlaceWarnings() const;
|
||||
std::expected<void, QString> saveAs( const QString& directory );
|
||||
std::expected<QString, QString> editDirectory();
|
||||
QByteArray definitionJsonForSave() const;
|
||||
std::expected<RunSource, QString> prepareRunSource( const QString& runDirectory );
|
||||
QJsonObject inputValuesForSave() const;
|
||||
|
||||
// Validation of the current revision (static checks plus `taskmaestro workflow describe`)
|
||||
void setValidationResult( int revision, const std::vector<RimWorkflowIssue>& issues, bool describeSucceeded );
|
||||
std::vector<RimWorkflowIssue> validationIssues() const;
|
||||
bool isValidationCurrent() const;
|
||||
bool lastValidationFailed() const;
|
||||
|
||||
std::vector<RimWorkflowJob*> jobs() const;
|
||||
void addJob( RimWorkflowJob* job );
|
||||
void syncJobs( const QMap<QString, QString>& renames = {} );
|
||||
|
||||
static QString findPythonExecutable();
|
||||
|
||||
// Copy the Python modules and packages of a workflow folder. Existing files are kept.
|
||||
static std::expected<void, QString> copyPythonModules( const QString& sourceDirectory, const QString& targetDirectory );
|
||||
|
||||
protected:
|
||||
void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override;
|
||||
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
|
||||
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
|
||||
|
||||
private:
|
||||
struct UndoEntry
|
||||
{
|
||||
RimWorkflowDefinition definition;
|
||||
QMap<QString, QString> renames;
|
||||
};
|
||||
|
||||
bool setLoadError( const QString& message, QString* errorMessage );
|
||||
void setDefinition( const RimWorkflowDefinition& definition, Source source );
|
||||
void definitionChanged( const QMap<QString, QString>& renames );
|
||||
void notifyDefinitionChanged();
|
||||
void rebuildGraph();
|
||||
void updateUiName();
|
||||
void ensureDefaultJob();
|
||||
|
||||
std::expected<QJsonObject, QString> saveToDirectory( const QString& directory, bool backup );
|
||||
|
||||
void recordModificationTime();
|
||||
static QMap<QString, QString> invertedRenames( const QMap<QString, QString>& renames );
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_name;
|
||||
caf::PdmField<QString> m_description;
|
||||
caf::PdmField<caf::FilePath> m_workflowDirectory;
|
||||
caf::PdmField<QString> m_registeredId;
|
||||
caf::PdmField<bool> m_editable;
|
||||
caf::PdmField<QString> m_readOnlyReason;
|
||||
caf::PdmField<QString> m_loadError;
|
||||
caf::PdmChildArrayField<RimWorkflowJob*> m_jobs;
|
||||
QJsonObject m_graph;
|
||||
|
||||
Source m_source = Source::YamlDirectory;
|
||||
QString m_sourceDirectory;
|
||||
RimWorkflowDefinition m_definition;
|
||||
RimWorkflowDefinition m_savedDefinition;
|
||||
int m_revision = 0;
|
||||
std::vector<UndoEntry> m_undoStack;
|
||||
std::vector<UndoEntry> m_redoStack;
|
||||
std::vector<RimWorkflowIssue> m_validationIssues;
|
||||
int m_validationRevision = -1;
|
||||
bool m_describeSucceeded = true;
|
||||
QDateTime m_loadedModificationTime;
|
||||
std::unique_ptr<QTemporaryDir> m_editDirectory;
|
||||
};
|
||||
@@ -47,7 +47,9 @@ void RimWorkflowArrayBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowArrayBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowArrayBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? m_value() : "null";
|
||||
if ( !hasValue() ) return QJsonValue::Null;
|
||||
const QJsonDocument document = QJsonDocument::fromJson( m_value().toUtf8() );
|
||||
return document.isArray() ? QJsonValue( document.array() ) : QJsonValue( m_value() );
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class RimWorkflowArrayBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowArrayBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -45,8 +45,7 @@ void RimWorkflowBoolBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowBoolBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowBoolBinding::toJsonValue() const
|
||||
{
|
||||
if ( !hasValue() ) return "null";
|
||||
return m_value() ? "true" : "false";
|
||||
return hasValue() ? QJsonValue( m_value() ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class RimWorkflowBoolBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowBoolBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
#include "RimWorkflowCaseBinding.h"
|
||||
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimEclipseCaseTools.h"
|
||||
#include "RimTools.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowCaseBinding, "WorkflowCaseBinding" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -42,10 +45,10 @@ QString RimWorkflowCaseBinding::displayValue() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowCaseBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowCaseBinding::toJsonValue() const
|
||||
{
|
||||
if ( m_case() == nullptr ) return "null";
|
||||
return QString( "{__resinsight_ref__: EclipseCase, case_id: %1}" ).arg( m_case()->caseId() );
|
||||
if ( m_case() == nullptr ) return QJsonValue::Null;
|
||||
return QJsonObject{ { "__resinsight_ref__", "EclipseCase" }, { "case_id", m_case()->caseId() } };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -57,3 +60,27 @@ QList<caf::PdmOptionItemInfo> RimWorkflowCaseBinding::calculateValueOptions( con
|
||||
if ( fieldNeedingOptions == &m_case ) RimTools::eclipseCaseOptionItems( &options );
|
||||
return options;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowCaseBinding::isObjectReference() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowCaseBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
{
|
||||
RimWorkflowFieldBinding::applySchema( fieldSchema );
|
||||
const QJsonObject reference = fieldSchema.value( "default" ).toObject();
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() != "EclipseCase" || !reference.contains( "case_id" ) ) return;
|
||||
|
||||
const int caseId = reference.value( "case_id" ).toInt( -1 );
|
||||
for ( RimEclipseCase* eclipseCase : RimEclipseCaseTools::eclipseCases() )
|
||||
{
|
||||
if ( eclipseCase && eclipseCase->caseId() == caseId ) m_case = eclipseCase;
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,10 @@ class RimWorkflowCaseBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowCaseBinding();
|
||||
|
||||
QString toYamlValue() const override;
|
||||
QString displayValue() const override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
bool isObjectReference() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString displayValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_case; }
|
||||
|
||||
|
||||
@@ -15,13 +15,20 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowCollection.h"
|
||||
|
||||
#include "RimWorkflowInstalledCollection.h"
|
||||
#include "RimWorkflowTaskCatalog.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
#include "RiaPreferencesSystem.h"
|
||||
|
||||
#include "cafCmdFeatureMenuBuilder.h"
|
||||
#include "cafPdmUiTreeOrdering.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowCollection, "WorkflowCollection" );
|
||||
|
||||
@@ -33,9 +40,13 @@ RimWorkflowCollection::RimWorkflowCollection()
|
||||
CAF_PDM_InitObject( "Workflows", ":/Folder.png" );
|
||||
CAF_PDM_InitFieldNoDefault( &m_items, "Workflows", "" );
|
||||
|
||||
rescanWorkflows();
|
||||
CAF_PDM_InitFieldNoDefault( &m_installed, "Installed", "" );
|
||||
m_installed = new RimWorkflowInstalledCollection;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowCollection::~RimWorkflowCollection() = default;
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -47,25 +58,103 @@ QString RimWorkflowCollection::discoveryDirectory()
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
/// Reload the task catalog, the workflow folders and the installed workflows. Workflows that
|
||||
/// have never been saved are kept. This runs the Python helper, so it is not done in the
|
||||
/// constructor: projects are also created in console mode and by code generation.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowCollection::rescanWorkflows()
|
||||
{
|
||||
deleteAllItems();
|
||||
for ( RimWorkflow* workflow : items() )
|
||||
{
|
||||
if ( workflow->source() != RimWorkflow::Source::Unsaved ) deleteItem( workflow );
|
||||
}
|
||||
m_installed->deleteAllItems();
|
||||
|
||||
if ( !RiaPreferencesSystem::current()->isFeatureEnabled( "workflows" ) ) return;
|
||||
|
||||
RimWorkflowTaskCatalog::invalidate();
|
||||
const RimWorkflowTaskCatalog& catalog = RimWorkflowTaskCatalog::instance();
|
||||
if ( !catalog.isValid() ) RiaLogging::warning( QString( "Workflow task catalog: %1" ).arg( catalog.loadError() ).toStdString() );
|
||||
for ( const QString& pluginError : catalog.pluginErrors() )
|
||||
RiaLogging::warning( QString( "Workflow plugin: %1" ).arg( pluginError ).toStdString() );
|
||||
|
||||
QDir dir( discoveryDirectory() );
|
||||
if ( !dir.exists() ) return;
|
||||
|
||||
const QFileInfoList entries = dir.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name );
|
||||
for ( const QFileInfo& entry : entries )
|
||||
if ( dir.exists() )
|
||||
{
|
||||
if ( !QFileInfo( entry.absoluteFilePath() + "/workflow.yaml" ).isFile() ) continue;
|
||||
const QFileInfoList entries = dir.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name );
|
||||
for ( const QFileInfo& entry : entries )
|
||||
{
|
||||
if ( !QFileInfo( entry.absoluteFilePath() + "/workflow.yaml" ).isFile() ) continue;
|
||||
|
||||
auto* workflow = new RimWorkflow;
|
||||
workflow->setWorkflowDirectory( entry.absoluteFilePath() );
|
||||
workflow->loadFromDirectory();
|
||||
addItem( workflow );
|
||||
auto* workflow = new RimWorkflow;
|
||||
workflow->setWorkflowDirectory( entry.absoluteFilePath() );
|
||||
workflow->loadFromDirectory();
|
||||
addItem( workflow );
|
||||
}
|
||||
}
|
||||
|
||||
for ( const QJsonValue& entry : catalog.registeredWorkflows() )
|
||||
{
|
||||
auto* workflow = new RimWorkflow;
|
||||
workflow->loadFromRegistered( entry.toObject() );
|
||||
m_installed->addItem( workflow );
|
||||
}
|
||||
|
||||
uiCapability()->updateConnectedEditors();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowCollection::hasUnsavedChanges() const
|
||||
{
|
||||
for ( RimWorkflow* workflow : items() )
|
||||
{
|
||||
if ( workflow->isEditable() && workflow->isDirty() ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflow*> RimWorkflowCollection::allWorkflows() const
|
||||
{
|
||||
std::vector<RimWorkflow*> workflows = items();
|
||||
for ( RimWorkflow* workflow : m_installed->items() )
|
||||
workflows.push_back( workflow );
|
||||
return workflows;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflow* RimWorkflowCollection::findWorkflowByDirectory( const QString& directory ) const
|
||||
{
|
||||
const QString wanted = QDir( directory ).absolutePath();
|
||||
for ( RimWorkflow* workflow : items() )
|
||||
{
|
||||
if ( !workflow->workflowDirectory().isEmpty() && QDir( workflow->workflowDirectory() ).absolutePath() == wanted ) return workflow;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const
|
||||
{
|
||||
menuBuilder << "RicNewWorkflowFeature";
|
||||
menuBuilder << "RicRescanWorkflowsFeature";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowCollection::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName )
|
||||
{
|
||||
for ( RimWorkflow* workflow : items() )
|
||||
uiTreeOrdering.add( workflow );
|
||||
if ( m_installed->count() > 0 ) uiTreeOrdering.add( m_installed() );
|
||||
uiTreeOrdering.skipRemainingChildren( true );
|
||||
}
|
||||
@@ -15,15 +15,21 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflow.h"
|
||||
|
||||
#include "cafPdmChildField.h"
|
||||
#include "cafPdmObjectCollection.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class RimWorkflowInstalledCollection;
|
||||
|
||||
//==================================================================================================
|
||||
/// Workflow folders in the discovery folder, new unsaved workflows, and the workflows registered
|
||||
/// by installed packages (in the "Installed" sub folder)
|
||||
//==================================================================================================
|
||||
class RimWorkflowCollection : public caf::PdmObjectCollection<RimWorkflow>
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
@@ -33,6 +39,17 @@ public:
|
||||
~RimWorkflowCollection() override;
|
||||
|
||||
void rescanWorkflows();
|
||||
bool hasUnsavedChanges() const;
|
||||
|
||||
std::vector<RimWorkflow*> allWorkflows() const;
|
||||
RimWorkflow* findWorkflowByDirectory( const QString& directory ) const;
|
||||
|
||||
static QString discoveryDirectory();
|
||||
|
||||
protected:
|
||||
void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override;
|
||||
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
|
||||
|
||||
private:
|
||||
caf::PdmChildField<RimWorkflowInstalledCollection*> m_installed;
|
||||
};
|
||||
@@ -46,7 +46,7 @@ void RimWorkflowDateBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowDateBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowDateBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? yamlQuotedScalar( m_value().toString( Qt::ISODate ) ) : "null";
|
||||
return hasValue() ? QJsonValue( m_value().toString( Qt::ISODate ) ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -29,8 +29,8 @@ class RimWorkflowDateBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowDateBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
|
||||
#include "RimWorkflowPortCompatibility.h"
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowDefinitionEdge::isWholeInput() const
|
||||
{
|
||||
return input.isEmpty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowDefinitionEdge::isWholeOutput() const
|
||||
{
|
||||
return output.isEmpty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowDefinitionEdge::isCollected() const
|
||||
{
|
||||
return collect != Collect::None;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Same tasks and ports, ignoring how the value is collected
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowDefinitionEdge::sameConnection( const RimWorkflowDefinitionEdge& other ) const
|
||||
{
|
||||
return from == other.from && output == other.output && to == other.to && input == other.input;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
const RimWorkflowDefinitionNode* RimWorkflowDefinition::findNode( const QString& nodeName ) const
|
||||
{
|
||||
for ( const auto& node : nodes )
|
||||
{
|
||||
if ( node.name == nodeName ) return &node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowDefinitionNode* RimWorkflowDefinition::findNode( const QString& nodeName )
|
||||
{
|
||||
for ( auto& node : nodes )
|
||||
{
|
||||
if ( node.name == nodeName ) return &node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
int RimWorkflowDefinition::nodeIndex( const QString& nodeName ) const
|
||||
{
|
||||
for ( size_t i = 0; i < nodes.size(); ++i )
|
||||
{
|
||||
if ( nodes[i].name == nodeName ) return static_cast<int>( i );
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowDefinition::taskType( const QString& taskId ) const
|
||||
{
|
||||
return taskTypes.value( taskId ).toObject();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The task type as seen by the workflow: a mapped task has `dict[str, Output]` as output, and its
|
||||
/// key and value fields are not inputs
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowDefinition::taskTypeForNode( const QString& nodeName ) const
|
||||
{
|
||||
const auto* node = findNode( nodeName );
|
||||
if ( !node ) return {};
|
||||
const QJsonObject type = taskType( node->taskId );
|
||||
if ( type.isEmpty() || !node->map ) return type;
|
||||
return RimWorkflowPortCompatibility::mappedTaskType( type, *node->map );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflowDefinitionEdge> RimWorkflowDefinition::incomingEdges( const QString& nodeName ) const
|
||||
{
|
||||
std::vector<RimWorkflowDefinitionEdge> result;
|
||||
for ( const auto& edge : edges )
|
||||
{
|
||||
if ( edge.to == nodeName ) result.push_back( edge );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflowDefinitionEdge> RimWorkflowDefinition::outgoingEdges( const QString& nodeName ) const
|
||||
{
|
||||
std::vector<RimWorkflowDefinitionEdge> result;
|
||||
for ( const auto& edge : edges )
|
||||
{
|
||||
if ( edge.from == nodeName ) result.push_back( edge );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowDefinition::nodeNames() const
|
||||
{
|
||||
QStringList names;
|
||||
for ( const auto& node : nodes )
|
||||
names.append( node.name );
|
||||
return names;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
//==================================================================================================
|
||||
/// A mapped task (`map:` in YAML) runs once per entry of the job config value `over`, a mapping
|
||||
/// with string keys. Each key goes to the input field `keyAs` and each value to `valueAs`.
|
||||
/// The output is `dict[str, Output]` keyed like `over`.
|
||||
//==================================================================================================
|
||||
struct RimWorkflowTaskMap
|
||||
{
|
||||
QString over{};
|
||||
QString keyAs{};
|
||||
QString valueAs{};
|
||||
QString errorMode = "fail_fast";
|
||||
|
||||
bool operator==( const RimWorkflowTaskMap& other ) const = default;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// One task instance in a workflow
|
||||
//==================================================================================================
|
||||
struct RimWorkflowDefinitionNode
|
||||
{
|
||||
QString name{};
|
||||
QString taskId{};
|
||||
QStringList configFields{};
|
||||
|
||||
// Fields the user wants to configure even when they are optional or cannot be derived
|
||||
QStringList explicitConfigFields{};
|
||||
|
||||
std::optional<RimWorkflowTaskMap> map{};
|
||||
|
||||
// Keys of the YAML entry the editor does not model (nested workflow); kept for read-only display
|
||||
QJsonObject extra{};
|
||||
|
||||
bool operator==( const RimWorkflowDefinitionNode& other ) const = default;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// A connection between two tasks. An empty output or input means the whole model.
|
||||
/// Several edges can collect outputs into one `list[T]` or `dict[str, T]` input (`collect:` in
|
||||
/// YAML). List members are ordered like the edges; dict members have a key.
|
||||
//==================================================================================================
|
||||
struct RimWorkflowDefinitionEdge
|
||||
{
|
||||
enum class Collect
|
||||
{
|
||||
None,
|
||||
List,
|
||||
Dict
|
||||
};
|
||||
|
||||
QString from{};
|
||||
QString output{};
|
||||
QString to{};
|
||||
QString input{};
|
||||
Collect collect = Collect::None;
|
||||
QString key{};
|
||||
|
||||
bool isWholeInput() const;
|
||||
bool isWholeOutput() const;
|
||||
bool isCollected() const;
|
||||
bool sameConnection( const RimWorkflowDefinitionEdge& other ) const;
|
||||
|
||||
bool operator==( const RimWorkflowDefinitionEdge& other ) const = default;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// A problem found by static validation or by `taskmaestro workflow describe`
|
||||
//==================================================================================================
|
||||
struct RimWorkflowIssue
|
||||
{
|
||||
enum class Severity
|
||||
{
|
||||
Error,
|
||||
Warning
|
||||
};
|
||||
|
||||
QString task{};
|
||||
QString field{};
|
||||
Severity severity = Severity::Error;
|
||||
QString message{};
|
||||
|
||||
bool operator==( const RimWorkflowIssue& other ) const = default;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// Editable model of a taskmaestro workflow, exchanged as JSON with `rips.taskmaestro_helper`.
|
||||
/// A plain value type: copies are cheap enough to serve as undo snapshots.
|
||||
//==================================================================================================
|
||||
struct RimWorkflowDefinition
|
||||
{
|
||||
int format = 1;
|
||||
QString name;
|
||||
QString resultTask;
|
||||
QString headerComment;
|
||||
QJsonObject passthrough;
|
||||
QJsonObject source;
|
||||
|
||||
std::vector<RimWorkflowDefinitionNode> nodes;
|
||||
std::vector<RimWorkflowDefinitionEdge> edges;
|
||||
|
||||
// Task instance name -> { field: value }
|
||||
QJsonObject inputs;
|
||||
|
||||
// Task id -> descriptor { id, name, python_type, description, input_schema, output_schema }
|
||||
QJsonObject taskTypes;
|
||||
|
||||
bool editable = true;
|
||||
QStringList readOnlyReasons;
|
||||
QStringList warnings;
|
||||
|
||||
QJsonObject describe;
|
||||
QJsonObject describeError;
|
||||
|
||||
const RimWorkflowDefinitionNode* findNode( const QString& nodeName ) const;
|
||||
RimWorkflowDefinitionNode* findNode( const QString& nodeName );
|
||||
int nodeIndex( const QString& nodeName ) const;
|
||||
QJsonObject taskType( const QString& taskId ) const;
|
||||
QJsonObject taskTypeForNode( const QString& nodeName ) const;
|
||||
std::vector<RimWorkflowDefinitionEdge> incomingEdges( const QString& nodeName ) const;
|
||||
std::vector<RimWorkflowDefinitionEdge> outgoingEdges( const QString& nodeName ) const;
|
||||
QStringList nodeNames() const;
|
||||
|
||||
bool operator==( const RimWorkflowDefinition& other ) const = default;
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,89 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
class RimWorkflowTaskCatalog;
|
||||
|
||||
//==================================================================================================
|
||||
/// Pure functions on RimWorkflowDefinition: JSON conversion, config derivation, edit operations,
|
||||
/// static validation and the graph JSON shown by RiuWorkflowGraphView.
|
||||
/// Edit operations return the new definition, or an error message for the user.
|
||||
//==================================================================================================
|
||||
namespace RimWorkflowDefinitionTools
|
||||
{
|
||||
using EditResult = std::expected<RimWorkflowDefinition, QString>;
|
||||
|
||||
constexpr const char* connectTaskId = "resinsight.connect";
|
||||
|
||||
// Conversion
|
||||
std::expected<RimWorkflowDefinition, QString> fromJson( const QJsonObject& json );
|
||||
QJsonObject toJson( const RimWorkflowDefinition& definition );
|
||||
RimWorkflowDefinition createEmpty( const QString& name );
|
||||
RimWorkflowDefinition editableCopy( const RimWorkflowDefinition& definition, const QString& name );
|
||||
|
||||
// Names
|
||||
bool isIdentifier( const QString& name );
|
||||
QString identifierFrom( const QString& text );
|
||||
QString uniqueInstanceName( const RimWorkflowDefinition& definition, const QString& baseName );
|
||||
std::expected<void, QString>
|
||||
validateInstanceName( const RimWorkflowDefinition& definition, const QString& name, const QString& currentName = {} );
|
||||
QStringList sinkTasks( const RimWorkflowDefinition& definition );
|
||||
QString effectiveResultTask( const RimWorkflowDefinition& definition );
|
||||
QStringList structuralReadOnlyReasons( const RimWorkflowDefinition& definition );
|
||||
|
||||
// Config fields
|
||||
QStringList coveredInputFields( const RimWorkflowDefinition& definition, const QString& nodeName );
|
||||
QStringList deriveConfigFields( const RimWorkflowDefinition& definition, const QString& nodeName );
|
||||
RimWorkflowDefinition normalize( RimWorkflowDefinition definition );
|
||||
|
||||
// Edit operations
|
||||
EditResult addTask( const RimWorkflowDefinition& definition,
|
||||
const RimWorkflowTaskCatalog& catalog,
|
||||
const QString& taskId,
|
||||
QString* addedName = nullptr );
|
||||
EditResult removeTask( const RimWorkflowDefinition& definition, const QString& nodeName );
|
||||
EditResult connect( const RimWorkflowDefinition& definition, const RimWorkflowDefinitionEdge& edge );
|
||||
EditResult disconnect( const RimWorkflowDefinition& definition, const RimWorkflowDefinitionEdge& edge );
|
||||
EditResult setCollectKey( const RimWorkflowDefinition& definition, const RimWorkflowDefinitionEdge& edge, const QString& key );
|
||||
EditResult moveCollectMember( const RimWorkflowDefinition& definition, const RimWorkflowDefinitionEdge& edge, int delta );
|
||||
EditResult setTaskMap( const RimWorkflowDefinition& definition, const QString& nodeName, const std::optional<RimWorkflowTaskMap>& map );
|
||||
std::expected<void, QString> checkTaskMap( const RimWorkflowDefinition& definition, const QString& nodeName, const RimWorkflowTaskMap& map );
|
||||
QJsonObject mapOverSchema( const RimWorkflowDefinition& definition, const QString& nodeName );
|
||||
EditResult renameTask( const RimWorkflowDefinition& definition, const QString& oldName, const QString& newName );
|
||||
EditResult renameWorkflow( const RimWorkflowDefinition& definition, const QString& newName );
|
||||
EditResult setResultTask( const RimWorkflowDefinition& definition, const QString& nodeName );
|
||||
EditResult setOptionalInputConfigured( const RimWorkflowDefinition& definition, const QString& nodeName, const QString& field, bool configured );
|
||||
RimWorkflowDefinition
|
||||
autoWireResInsightInputs( const RimWorkflowDefinition& definition, const RimWorkflowTaskCatalog& catalog, const QString& nodeName );
|
||||
|
||||
// Validation and display
|
||||
std::vector<RimWorkflowIssue> validate( const RimWorkflowDefinition& definition );
|
||||
QJsonObject graphFromDefinition( const RimWorkflowDefinition& definition, bool editMode, const std::vector<RimWorkflowIssue>& extraIssues = {} );
|
||||
} // namespace RimWorkflowDefinitionTools
|
||||
@@ -18,157 +18,15 @@
|
||||
|
||||
#include "RimWorkflowDescribeTools.h"
|
||||
|
||||
#include "RimWorkflowPortCompatibility.h"
|
||||
#include "RimWorkflowSchemaTools.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QMap>
|
||||
#include <QStringList>
|
||||
|
||||
namespace
|
||||
{
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject resolveRef( const QString& ref, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QString prefix = "#/$defs/";
|
||||
if ( !ref.startsWith( prefix ) ) return {};
|
||||
return rootSchema.value( "$defs" ).toObject().value( ref.mid( prefix.size() ) ).toObject();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Follow $ref and unwrap single-item allOf and Optional (anyOf with null) to the underlying type
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject underlyingSchema( QJsonObject schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
constexpr int maxDepth = 16;
|
||||
for ( int depth = 0; depth < maxDepth; ++depth )
|
||||
{
|
||||
if ( schema.contains( "$ref" ) )
|
||||
{
|
||||
schema = resolveRef( schema.value( "$ref" ).toString(), rootSchema );
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray allOf = schema.value( "allOf" ).toArray();
|
||||
if ( allOf.size() == 1 )
|
||||
{
|
||||
schema = allOf.first().toObject();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( schema.value( "anyOf" ).isArray() )
|
||||
{
|
||||
QJsonArray nonNull;
|
||||
for ( const QJsonValue& arm : schema.value( "anyOf" ).toArray() )
|
||||
{
|
||||
if ( arm.toObject().value( "type" ).toString() != "null" ) nonNull.append( arm );
|
||||
}
|
||||
if ( nonNull.size() == 1 )
|
||||
{
|
||||
schema = nonNull.first().toObject();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool isOpaque( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
return underlyingSchema( schema, rootSchema ).value( "x-taskmaestro-opaque" ).toBool( false );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Python type of a runtime-only object, either bare or wrapped in an ObjectModel `value` field
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString objectPythonType( const QJsonObject& typeSchema, const QJsonObject& rootSchema )
|
||||
{
|
||||
if ( typeSchema.contains( "x-taskmaestro-python-type" ) ) return typeSchema.value( "x-taskmaestro-python-type" ).toString();
|
||||
|
||||
const QJsonObject valueSchema = typeSchema.value( "properties" ).toObject().value( "value" ).toObject();
|
||||
if ( valueSchema.isEmpty() ) return {};
|
||||
return underlyingSchema( valueSchema, rootSchema ).value( "x-taskmaestro-python-type" ).toString();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString jsonType( const QJsonObject& typeSchema )
|
||||
{
|
||||
const QString type = typeSchema.value( "type" ).toString();
|
||||
if ( type == "boolean" || type == "integer" || type == "number" || type == "array" ) return type;
|
||||
return "string";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// taskmaestro_resinsight.models.Vec3 is a plain pydantic model (x, y, z numbers), so it has no
|
||||
/// x-taskmaestro-python-type tag; recognize it structurally instead.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool isVec3Schema( const QJsonObject& typeSchema )
|
||||
{
|
||||
if ( typeSchema.value( "type" ).toString() != "object" ) return false;
|
||||
|
||||
const QJsonObject properties = typeSchema.value( "properties" ).toObject();
|
||||
if ( properties.size() != 3 ) return false;
|
||||
|
||||
for ( const QString& component : QStringList{ "x", "y", "z" } )
|
||||
{
|
||||
if ( properties.value( component ).toObject().value( "type" ).toString() != "number" ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject configFieldSchema( const QString& fieldName,
|
||||
const QJsonObject& inputSchema,
|
||||
const QStringList& requiredFields,
|
||||
const QJsonObject& configValues )
|
||||
{
|
||||
QJsonObject entry{ { "name", fieldName } };
|
||||
|
||||
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
||||
if ( !properties.contains( fieldName ) )
|
||||
{
|
||||
entry["type"] = "string";
|
||||
entry["required"] = true;
|
||||
entry["error"] = "field not found in input model";
|
||||
return entry;
|
||||
}
|
||||
|
||||
const QJsonObject property = properties.value( fieldName ).toObject();
|
||||
const QJsonObject typeSchema = underlyingSchema( property, inputSchema );
|
||||
QString resinsightType = RimWorkflowDescribeTools::resinsightTypeFromPythonType( objectPythonType( typeSchema, inputSchema ) );
|
||||
if ( resinsightType.isEmpty() && isVec3Schema( typeSchema ) ) resinsightType = "Vec3";
|
||||
|
||||
entry["type"] = resinsightType.isEmpty() ? jsonType( typeSchema ) : "object";
|
||||
entry["required"] = requiredFields.contains( fieldName );
|
||||
|
||||
const QString description = property.value( "description" ).toString();
|
||||
if ( !description.isEmpty() ) entry["description"] = description;
|
||||
|
||||
// Prefer the value already in input.yaml; fall back to the model default
|
||||
if ( configValues.contains( fieldName ) )
|
||||
entry["default"] = configValues.value( fieldName );
|
||||
else if ( property.contains( "default" ) )
|
||||
entry["default"] = property.value( "default" );
|
||||
|
||||
const QString format = typeSchema.value( "format" ).toString();
|
||||
if ( !format.isEmpty() ) entry["format"] = format;
|
||||
|
||||
if ( !resinsightType.isEmpty() ) entry["resinsight_type"] = resinsightType;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void appendEdge( QJsonArray& edges, const QJsonObject& ref, const QString& to, const QString& input )
|
||||
{
|
||||
const QString from = ref.value( "task" ).toString();
|
||||
@@ -236,7 +94,7 @@ QJsonObject taskFromDescribe( const QJsonObject& task )
|
||||
const QJsonObject outputProperties = outputSchema.value( "properties" ).toObject();
|
||||
for ( auto it = outputProperties.begin(); it != outputProperties.end(); ++it )
|
||||
{
|
||||
if ( it.key() == "value" && isOpaque( it.value().toObject(), outputSchema ) ) continue;
|
||||
if ( it.key() == "value" && RimWorkflowSchemaTools::isOpaque( it.value().toObject(), outputSchema ) ) continue;
|
||||
outputs.append( it.key() );
|
||||
}
|
||||
|
||||
@@ -251,13 +109,17 @@ QJsonObject taskFromDescribe( const QJsonObject& task )
|
||||
{
|
||||
const QString fieldName = value.toString();
|
||||
if ( fieldName.isEmpty() ) continue;
|
||||
configFields.append( configFieldSchema( fieldName, inputSchema, requiredFields, configValues ) );
|
||||
configFields.append( RimWorkflowSchemaTools::configFieldSchema( fieldName, inputSchema, requiredFields, configValues ) );
|
||||
}
|
||||
|
||||
return QJsonObject{ { "name", task.value( "name" ).toString() },
|
||||
{ "inputs", inputs },
|
||||
{ "outputs", outputs },
|
||||
{ "config_fields", configFields } };
|
||||
{ "config_fields", configFields },
|
||||
{ "input_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::inputPorts( task ) ) },
|
||||
{ "output_types", RimWorkflowPortCompatibility::portTypes( RimWorkflowPortCompatibility::outputPorts( task ) ) },
|
||||
{ "input_icons", RimWorkflowPortCompatibility::portIcons( RimWorkflowPortCompatibility::inputPorts( task ) ) },
|
||||
{ "output_icons", RimWorkflowPortCompatibility::portIcons( RimWorkflowPortCompatibility::outputPorts( task ) ) } };
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -304,17 +166,5 @@ QString RimWorkflowDescribeTools::errorFromDescribe( const QJsonObject& describe
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowDescribeTools::resinsightTypeFromPythonType( const QString& pythonType )
|
||||
{
|
||||
const int separator = pythonType.lastIndexOf( '.' );
|
||||
if ( separator < 0 ) return {};
|
||||
|
||||
const QString module = pythonType.left( separator );
|
||||
if ( module != "rips" && !module.startsWith( "rips." ) ) return {};
|
||||
|
||||
static const QMap<QString, QString> typeMap = { { "Case", "EclipseCase" },
|
||||
{ "Reservoir", "EclipseCase" },
|
||||
{ "EclipseCase", "EclipseCase" },
|
||||
{ "WellPath", "WellPath" },
|
||||
{ "View", "View" },
|
||||
{ "EclipseView", "View" } };
|
||||
return typeMap.value( pythonType.mid( separator + 1 ) );
|
||||
return RimWorkflowSchemaTools::resinsightTypeFromPythonType( pythonType );
|
||||
}
|
||||
@@ -121,10 +121,9 @@ bool RimWorkflowFieldBinding::hasValue() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowFieldBinding::yamlQuotedScalar( const QString& value )
|
||||
bool RimWorkflowFieldBinding::isObjectReference() const
|
||||
{
|
||||
const QByteArray encoded = QJsonDocument( QJsonArray{ value } ).toJson( QJsonDocument::Compact );
|
||||
return QString::fromUtf8( encoded.mid( 1, encoded.size() - 2 ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
#include <QJsonValue>
|
||||
|
||||
class QJsonObject;
|
||||
|
||||
class RimWorkflowFieldBinding : public caf::PdmObject
|
||||
@@ -35,17 +37,17 @@ public:
|
||||
void setDescription( const QString& description );
|
||||
void setRequired( bool required );
|
||||
|
||||
virtual void applySchema( const QJsonObject& fieldSchema );
|
||||
virtual QString toYamlValue() const = 0;
|
||||
virtual QString displayValue() const;
|
||||
virtual void applySchema( const QJsonObject& fieldSchema );
|
||||
virtual QJsonValue toJsonValue() const = 0;
|
||||
virtual bool isObjectReference() const;
|
||||
virtual QString displayValue() const;
|
||||
|
||||
virtual caf::PdmFieldHandle* valueField() = 0;
|
||||
|
||||
protected:
|
||||
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
|
||||
|
||||
bool hasValue() const;
|
||||
static QString yamlQuotedScalar( const QString& value );
|
||||
bool hasValue() const;
|
||||
|
||||
caf::PdmField<QString> m_fieldName;
|
||||
caf::PdmField<QString> m_description;
|
||||
|
||||
@@ -63,7 +63,7 @@ QString RimWorkflowFilePathBinding::displayValue() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowFilePathBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowFilePathBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? yamlQuotedScalar( m_value().path() ) : "null";
|
||||
return hasValue() ? QJsonValue( m_value().path() ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -29,9 +29,9 @@ class RimWorkflowFilePathBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowFilePathBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
QString displayValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
QString displayValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ void RimWorkflowFloatBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowFloatBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowFloatBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? QString::number( m_value(), 'g', 17 ) : "null";
|
||||
return hasValue() ? QJsonValue( m_value() ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class RimWorkflowFloatBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowFloatBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowHelperProcess.h"
|
||||
|
||||
#include "RimWorkflow.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
#include <QProcess>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int startTimeoutMs = 10000;
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QJsonObject, QString>
|
||||
RimWorkflowHelperProcess::runSync( const QStringList& args, const QByteArray& stdinData, int timeoutMs, const QString& extraPythonPath )
|
||||
{
|
||||
const QString python = RimWorkflow::findPythonExecutable();
|
||||
if ( python.isEmpty() ) return std::unexpected( "No usable Python interpreter found" );
|
||||
|
||||
QProcess process;
|
||||
process.setProcessEnvironment( environment( extraPythonPath ) );
|
||||
process.start( python, helperArguments( args ) );
|
||||
if ( !process.waitForStarted( startTimeoutMs ) ) return std::unexpected( QString( "Could not launch '%1'" ).arg( python ) );
|
||||
|
||||
if ( !stdinData.isEmpty() ) process.write( stdinData );
|
||||
process.closeWriteChannel();
|
||||
|
||||
if ( !process.waitForFinished( timeoutMs ) )
|
||||
{
|
||||
process.kill();
|
||||
process.waitForFinished( startTimeoutMs );
|
||||
return std::unexpected( QString( "The workflow helper timed out (%1)" ).arg( args.value( 0 ) ) );
|
||||
}
|
||||
|
||||
const int exitCode = process.exitStatus() == QProcess::NormalExit ? process.exitCode() : -1;
|
||||
return parseOutput( exitCode, process.readAllStandardOutput(), process.readAllStandardError() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowHelperProcess::helperArguments( const QStringList& args )
|
||||
{
|
||||
return QStringList{ "-m", "rips.taskmaestro_helper" } + args;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The process environment, with an extra folder in front of PYTHONPATH (to import workflow-local modules)
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QProcessEnvironment RimWorkflowHelperProcess::environment( const QString& extraPythonPath )
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
if ( extraPythonPath.isEmpty() ) return env;
|
||||
|
||||
const QString existing = env.value( "PYTHONPATH" );
|
||||
const QString path = QDir::toNativeSeparators( extraPythonPath );
|
||||
env.insert( "PYTHONPATH", existing.isEmpty() ? path : path + QDir::listSeparator() + existing );
|
||||
return env;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<QJsonObject, QString>
|
||||
RimWorkflowHelperProcess::parseOutput( int exitCode, const QByteArray& standardOutput, const QByteArray& standardError )
|
||||
{
|
||||
if ( const auto invalid = invalidResult( standardOutput ) )
|
||||
return std::unexpected( errorMessage( invalid->value( "error" ).toObject() ) );
|
||||
|
||||
QJsonParseError parseError{};
|
||||
const QJsonDocument document = QJsonDocument::fromJson( standardOutput.trimmed(), &parseError );
|
||||
if ( parseError.error == QJsonParseError::NoError && document.isObject() && exitCode == 0 ) return document.object();
|
||||
|
||||
QString message = QString::fromUtf8( standardError ).trimmed();
|
||||
if ( message.isEmpty() ) message = QString::fromUtf8( standardOutput ).trimmed();
|
||||
// The last lines of a traceback hold the exception
|
||||
const QStringList lines = message.split( '\n', Qt::SkipEmptyParts );
|
||||
if ( lines.size() > 6 ) message = lines.mid( lines.size() - 6 ).join( '\n' );
|
||||
if ( message.isEmpty() ) message = QString( "The workflow helper failed with exit code %1" ).arg( exitCode );
|
||||
return std::unexpected( message );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<QJsonObject> RimWorkflowHelperProcess::invalidResult( const QByteArray& standardOutput )
|
||||
{
|
||||
QJsonParseError parseError{};
|
||||
const QJsonDocument document = QJsonDocument::fromJson( standardOutput.trimmed(), &parseError );
|
||||
if ( parseError.error != QJsonParseError::NoError || !document.isObject() ) return std::nullopt;
|
||||
if ( document.object().value( "status" ).toString() != "invalid" ) return std::nullopt;
|
||||
return document.object();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Message for an error object { message, task, field }
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowHelperProcess::errorMessage( const QJsonObject& error )
|
||||
{
|
||||
QString message = error.value( "message" ).toString();
|
||||
if ( message.isEmpty() ) message = "Workflow configuration is invalid";
|
||||
|
||||
QStringList details;
|
||||
const QString task = error.value( "task" ).toString();
|
||||
if ( !task.isEmpty() && !message.contains( task ) ) details.append( QString( "task '%1'" ).arg( task ) );
|
||||
const QString field = error.value( "field" ).toString();
|
||||
if ( !field.isEmpty() && !message.contains( field ) ) details.append( QString( "field '%1'" ).arg( field ) );
|
||||
if ( !details.isEmpty() ) message += " (" + details.join( ", " ) + ")";
|
||||
return message;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
|
||||
//==================================================================================================
|
||||
/// Runs `python -m rips.taskmaestro_helper <args>`, which prints exactly one JSON document
|
||||
//==================================================================================================
|
||||
namespace RimWorkflowHelperProcess
|
||||
{
|
||||
constexpr int defaultTimeoutMs = 60000;
|
||||
|
||||
std::expected<QJsonObject, QString>
|
||||
runSync( const QStringList& args, const QByteArray& stdinData = {}, int timeoutMs = defaultTimeoutMs, const QString& extraPythonPath = {} );
|
||||
|
||||
QStringList helperArguments( const QStringList& args );
|
||||
QProcessEnvironment environment( const QString& extraPythonPath );
|
||||
std::expected<QJsonObject, QString> parseOutput( int exitCode, const QByteArray& standardOutput, const QByteArray& standardError );
|
||||
|
||||
// The `{"status": "invalid", "error": {...}}` document printed on stdout, if any. parseOutput()
|
||||
// reduces it to a message; this keeps the task and field the error refers to.
|
||||
std::optional<QJsonObject> invalidResult( const QByteArray& standardOutput );
|
||||
QString errorMessage( const QJsonObject& error );
|
||||
} // namespace RimWorkflowHelperProcess
|
||||
@@ -0,0 +1,29 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026 Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
#include "RimWorkflowInstalledCollection.h"
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowInstalledCollection, "WorkflowInstalledCollection" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowInstalledCollection::RimWorkflowInstalledCollection()
|
||||
{
|
||||
CAF_PDM_InitObject( "Installed", ":/Folder.png" );
|
||||
CAF_PDM_InitFieldNoDefault( &m_items, "Workflows", "" );
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026 Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflow.h"
|
||||
|
||||
#include "cafPdmObjectCollection.h"
|
||||
|
||||
//==================================================================================================
|
||||
/// Workflows registered by installed Python packages (read-only)
|
||||
//==================================================================================================
|
||||
class RimWorkflowInstalledCollection : public caf::PdmObjectCollection<RimWorkflow>
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimWorkflowInstalledCollection();
|
||||
};
|
||||
@@ -45,7 +45,7 @@ void RimWorkflowIntBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowIntBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowIntBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? QString::number( m_value() ) : "null";
|
||||
return hasValue() ? QJsonValue( m_value() ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class RimWorkflowIntBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowIntBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "RimWorkflowJob.h"
|
||||
|
||||
#include "RimWorkflow.h"
|
||||
#include "RimWorkflowHelperProcess.h"
|
||||
|
||||
#include "RiaApplication.h"
|
||||
#include "RiaLogging.h"
|
||||
@@ -34,8 +35,7 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QUuid>
|
||||
|
||||
@@ -87,7 +87,7 @@ void RimWorkflowJob::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering&
|
||||
|
||||
for ( RimWorkflowTaskInput* task : m_taskInputs.childrenByType() )
|
||||
{
|
||||
if ( !task ) continue;
|
||||
if ( !task || task->count() == 0 ) continue;
|
||||
caf::PdmUiGroup* group = uiOrdering.addNewGroup( task->taskName() );
|
||||
for ( RimWorkflowFieldBinding* b : task->items() )
|
||||
{
|
||||
@@ -212,6 +212,29 @@ void RimWorkflowJob::updateTaskState( const QString& runId, const QString& taskN
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QMap<QString, QMap<QString, QString>> RimWorkflowJob::itemStates() const
|
||||
{
|
||||
return m_itemStates;
|
||||
}
|
||||
|
||||
QMap<QString, QMap<QString, QString>> RimWorkflowJob::itemErrors() const
|
||||
{
|
||||
return m_itemErrors;
|
||||
}
|
||||
|
||||
void RimWorkflowJob::updateItemState( const QString& runId, const QString& taskName, const QString& item, const QString& state, const QString& error )
|
||||
{
|
||||
if ( runId != m_runId || !m_taskStates.contains( taskName ) ) return;
|
||||
if ( state != "running" && state != "completed" && state != "failed" ) return;
|
||||
|
||||
m_itemStates[taskName][item] = state;
|
||||
if ( state == "failed" )
|
||||
m_itemErrors[taskName][item] = error;
|
||||
else if ( m_itemErrors.contains( taskName ) )
|
||||
m_itemErrors[taskName].remove( item );
|
||||
if ( auto* window = RiuMainWindow::instance() ) window->workflowJobStateChanged( this );
|
||||
}
|
||||
|
||||
void RimWorkflowJob::finishRun( const QString& runId, bool succeeded, bool cancelled )
|
||||
{
|
||||
if ( runId != m_runId ) return;
|
||||
@@ -226,7 +249,7 @@ void RimWorkflowJob::finishRun( const QString& runId, bool succeeded, bool cance
|
||||
if ( cancelled )
|
||||
m_runStatus = "Cancelled";
|
||||
else if ( succeeded )
|
||||
m_runStatus = "Completed";
|
||||
m_runStatus.clear();
|
||||
else if ( !m_runStatus.startsWith( "Failed:" ) )
|
||||
m_runStatus = "Failed (see workflow log)";
|
||||
m_activeTask.clear();
|
||||
@@ -244,17 +267,132 @@ void RimWorkflowJob::cancelJob()
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowJob::writeInputYaml( const QString& path ) const
|
||||
RimWorkflowTaskInput* RimWorkflowJob::taskInput( const QString& taskName ) const
|
||||
{
|
||||
QString body;
|
||||
for ( RimWorkflowTaskInput* t : m_taskInputs.childrenByType() )
|
||||
for ( RimWorkflowTaskInput* input : taskInputs() )
|
||||
{
|
||||
if ( t ) body += t->toTaskYamlBlock();
|
||||
if ( input->taskName() == taskName ) return input;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Make the task inputs follow the tasks of the workflow graph. Inputs of renamed tasks
|
||||
/// (`renames` maps old to new names) keep their values; values of removed fields are kept aside
|
||||
/// and restored if the field comes back.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowJob::syncTaskInputs( const QJsonArray& graphTasks, const QMap<QString, QString>& renames )
|
||||
{
|
||||
// Detached values are stored as "task/field/binding"
|
||||
QMap<QString, QJsonValue> detached;
|
||||
for ( auto it = m_detachedValues.begin(); it != m_detachedValues.end(); ++it )
|
||||
{
|
||||
const QString task = it.key().section( '/', 0, 0 );
|
||||
const QString rest = it.key().section( '/', 1 );
|
||||
detached.insert( renames.value( task, task ) + "/" + rest, it.value() );
|
||||
}
|
||||
|
||||
std::vector<RimWorkflowTaskInput*> existing = taskInputs();
|
||||
m_taskInputs.clearWithoutDelete();
|
||||
|
||||
auto takeExisting = [&existing, &renames]( const QString& name ) -> RimWorkflowTaskInput*
|
||||
{
|
||||
for ( auto it = existing.begin(); it != existing.end(); ++it )
|
||||
{
|
||||
const QString currentName = renames.value( ( *it )->taskName(), ( *it )->taskName() );
|
||||
if ( currentName == name )
|
||||
{
|
||||
RimWorkflowTaskInput* input = *it;
|
||||
existing.erase( it );
|
||||
return input;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
for ( const QJsonValue& value : graphTasks )
|
||||
{
|
||||
const QJsonObject task = value.toObject();
|
||||
const QString name = task.value( "name" ).toString();
|
||||
|
||||
RimWorkflowTaskInput* input = takeExisting( name );
|
||||
if ( !input ) input = new RimWorkflowTaskInput;
|
||||
input->setTaskName( name );
|
||||
input->setTaskInfo( task.value( "task_id" ).toString(), task.value( "description" ).toString() );
|
||||
|
||||
QMap<QString, QJsonValue> taskDetached;
|
||||
const QString prefix = name + "/";
|
||||
for ( auto it = detached.begin(); it != detached.end(); )
|
||||
{
|
||||
if ( it.key().startsWith( prefix ) )
|
||||
{
|
||||
taskDetached.insert( it.key().mid( prefix.size() ), it.value() );
|
||||
it = detached.erase( it );
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
input->syncFromSchema( task.value( "config_fields" ).toArray(), taskDetached );
|
||||
for ( auto it = taskDetached.begin(); it != taskDetached.end(); ++it )
|
||||
detached.insert( prefix + it.key(), it.value() );
|
||||
|
||||
m_taskInputs.push_back( input );
|
||||
}
|
||||
|
||||
// Inputs of removed tasks are kept aside, in case the removal is undone
|
||||
for ( RimWorkflowTaskInput* removed : existing )
|
||||
{
|
||||
const QString name = renames.value( removed->taskName(), removed->taskName() );
|
||||
for ( RimWorkflowFieldBinding* binding : removed->items() )
|
||||
{
|
||||
const QJsonValue value = binding->toJsonValue();
|
||||
if ( !value.isNull() )
|
||||
detached.insert( name + "/" + RimWorkflowTaskInput::detachedKey( binding->fieldName(), binding->classKeyword() ), value );
|
||||
}
|
||||
delete removed;
|
||||
}
|
||||
|
||||
m_detachedValues = detached;
|
||||
uiCapability()->updateConnectedEditors();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The values used to run the job, including references to ResInsight objects
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowJob::inputValues() const
|
||||
{
|
||||
QJsonObject values;
|
||||
for ( RimWorkflowTaskInput* input : taskInputs() )
|
||||
{
|
||||
if ( input->count() > 0 ) values.insert( input->taskName(), input->jsonValues() );
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Values that can be saved to input.yaml
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowJob::literalInputValues() const
|
||||
{
|
||||
QJsonObject values;
|
||||
for ( RimWorkflowTaskInput* input : taskInputs() )
|
||||
{
|
||||
if ( input->count() > 0 ) values.insert( input->taskName(), input->literalValues() );
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Write the input values as JSON, which is also valid YAML
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowJob::writeInputYaml( const QString& path ) const
|
||||
{
|
||||
QFile out( path );
|
||||
if ( !out.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) return {};
|
||||
out.write( body.toUtf8() );
|
||||
out.write( QJsonDocument( inputValues() ).toJson( QJsonDocument::Indented ) );
|
||||
out.close();
|
||||
return path;
|
||||
}
|
||||
@@ -277,9 +415,6 @@ void RimWorkflowJob::runJob()
|
||||
return;
|
||||
}
|
||||
|
||||
const QString dir = workflow->workflowDirectory();
|
||||
if ( dir.isEmpty() ) return;
|
||||
|
||||
auto port = RiaApplication::instance()->activeGrpcPortNumber();
|
||||
if ( !port.has_value() )
|
||||
{
|
||||
@@ -301,6 +436,13 @@ void RimWorkflowJob::runJob()
|
||||
RiaLogging::warning( "Cannot create temp dir for workflow run." );
|
||||
return;
|
||||
}
|
||||
|
||||
auto runSource = workflow->prepareRunSource( tmp.absoluteFilePath( runDir ) );
|
||||
if ( !runSource )
|
||||
{
|
||||
RiaLogging::warning( QString( "Cannot run workflow: %1" ).arg( runSource.error() ).toStdString() );
|
||||
return;
|
||||
}
|
||||
QString inputPath = tmp.absoluteFilePath( runDir + "/input.yaml" );
|
||||
if ( writeInputYaml( inputPath ).isEmpty() )
|
||||
{
|
||||
@@ -311,6 +453,8 @@ void RimWorkflowJob::runJob()
|
||||
m_runId = QUuid::createUuid().toString( QUuid::WithoutBraces );
|
||||
m_taskStates.clear();
|
||||
m_taskErrors.clear();
|
||||
m_itemStates.clear();
|
||||
m_itemErrors.clear();
|
||||
for ( const QJsonValue& value : workflow->graph().value( "tasks" ).toArray() )
|
||||
{
|
||||
const QString taskName = value.toObject().value( "name" ).toString();
|
||||
@@ -320,9 +464,10 @@ void RimWorkflowJob::runJob()
|
||||
m_runStatus = "Running";
|
||||
if ( auto* window = RiuMainWindow::instance() ) window->workflowJobStateChanged( this );
|
||||
|
||||
QStringList args{ "-m", "rips.taskmaestro_helper", "run", dir, "--input", inputPath, "--grpc-port", QString::number( port.value() ), "--run-id", m_runId };
|
||||
const QStringList args = RimWorkflowHelperProcess::helperArguments(
|
||||
runSource->baseArgs + QStringList{ "--input", inputPath, "--grpc-port", QString::number( port.value() ), "--run-id", m_runId } );
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
QProcessEnvironment env = RimWorkflowHelperProcess::environment( runSource->extraPythonPath );
|
||||
const QString label = workflow->uiName() + " / " + m_name();
|
||||
|
||||
m_runner = new RiuWorkflowJobRunner( label, RiuMainWindow::instance() );
|
||||
@@ -335,6 +480,14 @@ void RimWorkflowJob::runJob()
|
||||
{
|
||||
if ( safeJob && eventRunId == runId ) safeJob->updateTaskState( runId, taskName, state, error );
|
||||
} );
|
||||
QObject::connect( m_runner,
|
||||
&RiuWorkflowJobRunner::mapItemStateChanged,
|
||||
m_runner,
|
||||
[safeJob,
|
||||
runId]( const QString& eventRunId, const QString& taskName, const QString& item, const QString& state, const QString& error )
|
||||
{
|
||||
if ( safeJob && eventRunId == runId ) safeJob->updateItemState( runId, taskName, item, state, error );
|
||||
} );
|
||||
QObject::connect( m_runner,
|
||||
&RiuWorkflowJobRunner::runFinished,
|
||||
m_runner,
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#include "cafPdmField.h"
|
||||
#include "cafPdmObject.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QMap>
|
||||
#include <QPointer>
|
||||
|
||||
@@ -40,18 +43,27 @@ public:
|
||||
void setJobName( const QString& name );
|
||||
|
||||
std::vector<RimWorkflowTaskInput*> taskInputs() const;
|
||||
RimWorkflowTaskInput* taskInput( const QString& taskName ) const;
|
||||
void setTaskInputs( std::vector<RimWorkflowTaskInput*> inputs );
|
||||
void syncTaskInputs( const QJsonArray& graphTasks, const QMap<QString, QString>& renames = {} );
|
||||
|
||||
QString writeInputYaml( const QString& path ) const;
|
||||
void runJob();
|
||||
void cancelJob();
|
||||
bool isRunning() const;
|
||||
QJsonObject inputValues() const;
|
||||
QJsonObject literalInputValues() const;
|
||||
QString writeInputYaml( const QString& path ) const;
|
||||
void runJob();
|
||||
void cancelJob();
|
||||
bool isRunning() const;
|
||||
|
||||
QMap<QString, QString> taskStates() const;
|
||||
QMap<QString, QString> taskErrors() const;
|
||||
QString runStatus() const;
|
||||
void updateTaskState( const QString& runId, const QString& taskName, const QString& state, const QString& error );
|
||||
void finishRun( const QString& runId, bool succeeded, bool cancelled );
|
||||
|
||||
// States and errors of the items of mapped tasks, by task and item key
|
||||
QMap<QString, QMap<QString, QString>> itemStates() const;
|
||||
QMap<QString, QMap<QString, QString>> itemErrors() const;
|
||||
void updateItemState( const QString& runId, const QString& taskName, const QString& item, const QString& state, const QString& error );
|
||||
void finishRun( const QString& runId, bool succeeded, bool cancelled );
|
||||
|
||||
protected:
|
||||
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
|
||||
@@ -64,8 +76,11 @@ private:
|
||||
caf::PdmField<QString> m_name;
|
||||
caf::PdmChildArrayField<RimWorkflowTaskInput*> m_taskInputs;
|
||||
QPointer<RiuWorkflowJobRunner> m_runner;
|
||||
QMap<QString, QJsonValue> m_detachedValues;
|
||||
QMap<QString, QString> m_taskStates;
|
||||
QMap<QString, QString> m_taskErrors;
|
||||
QMap<QString, QMap<QString, QString>> m_itemStates;
|
||||
QMap<QString, QMap<QString, QString>> m_itemErrors;
|
||||
QString m_runStatus;
|
||||
QString m_activeTask;
|
||||
QString m_runId;
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowMappingBinding.h"
|
||||
|
||||
#include "RimEclipseCase.h"
|
||||
#include "RimEclipseCaseTools.h"
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimProject.h"
|
||||
#include "RimTools.h"
|
||||
#include "RimWellPath.h"
|
||||
|
||||
#include "cafPdmUiTreeSelectionEditor.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSet>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowMappingBinding, "WorkflowMappingBinding" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowMappingBinding::RimWorkflowMappingBinding()
|
||||
{
|
||||
CAF_PDM_InitFieldNoDefault( &m_valueType, "ValueType", "Value Type" );
|
||||
m_valueType.uiCapability()->setUiHidden( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_text, "Text", "Items" );
|
||||
m_text.uiCapability()->setUiToolTip( "A JSON object, for example {\"low\": 0.5, \"high\": 2.0}" );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_cases, "Cases", "Cases" );
|
||||
CAF_PDM_InitFieldNoDefault( &m_wellPaths, "WellPaths", "Well Paths" );
|
||||
CAF_PDM_InitFieldNoDefault( &m_views, "Views", "Views" );
|
||||
for ( caf::PdmFieldHandle* field : std::initializer_list<caf::PdmFieldHandle*>{ &m_cases, &m_wellPaths, &m_views } )
|
||||
{
|
||||
field->uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() );
|
||||
field->uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
caf::PdmFieldHandle* RimWorkflowMappingBinding::valueField()
|
||||
{
|
||||
if ( m_valueType() == "EclipseCase" ) return &m_cases;
|
||||
if ( m_valueType() == "WellPath" ) return &m_wellPaths;
|
||||
if ( m_valueType() == "View" ) return &m_views;
|
||||
return &m_text;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowMappingBinding::isObjectReference() const
|
||||
{
|
||||
return !m_valueType().isEmpty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The selected objects as `{key: reference}`, keyed by their names made unique
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::pair<QString, QJsonObject>> RimWorkflowMappingBinding::references() const
|
||||
{
|
||||
std::vector<std::pair<QString, QJsonObject>> items;
|
||||
if ( m_valueType() == "EclipseCase" )
|
||||
{
|
||||
for ( RimEclipseCase* eclipseCase : m_cases.ptrReferencedObjectsByType() )
|
||||
{
|
||||
if ( eclipseCase )
|
||||
items.push_back( { eclipseCase->caseUserDescription(),
|
||||
QJsonObject{ { "__resinsight_ref__", "EclipseCase" }, { "case_id", eclipseCase->caseId() } } } );
|
||||
}
|
||||
}
|
||||
else if ( m_valueType() == "WellPath" )
|
||||
{
|
||||
for ( RimWellPath* wellPath : m_wellPaths.ptrReferencedObjectsByType() )
|
||||
{
|
||||
if ( wellPath )
|
||||
items.push_back(
|
||||
{ wellPath->name(), QJsonObject{ { "__resinsight_ref__", "WellPath" }, { "well_path_name", wellPath->name() } } } );
|
||||
}
|
||||
}
|
||||
else if ( m_valueType() == "View" )
|
||||
{
|
||||
for ( RimEclipseView* view : m_views.ptrReferencedObjectsByType() )
|
||||
{
|
||||
if ( view ) items.push_back( { view->name(), QJsonObject{ { "__resinsight_ref__", "View" }, { "view_id", view->id() } } } );
|
||||
}
|
||||
}
|
||||
|
||||
QSet<QString> used;
|
||||
for ( auto& [key, reference] : items )
|
||||
{
|
||||
const QString base = key.isEmpty() ? QString( "item" ) : key;
|
||||
key = base;
|
||||
for ( int index = 2; used.contains( key ); ++index )
|
||||
key = QString( "%1_%2" ).arg( base ).arg( index );
|
||||
used.insert( key );
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonValue RimWorkflowMappingBinding::toJsonValue() const
|
||||
{
|
||||
if ( isObjectReference() )
|
||||
{
|
||||
QJsonObject mapping;
|
||||
for ( const auto& [key, reference] : references() )
|
||||
mapping[key] = reference;
|
||||
return mapping.isEmpty() ? QJsonValue( QJsonValue::Null ) : QJsonValue( mapping );
|
||||
}
|
||||
|
||||
if ( !hasValue() ) return QJsonValue::Null;
|
||||
const QJsonDocument document = QJsonDocument::fromJson( m_text().toUtf8() );
|
||||
return document.isObject() ? QJsonValue( document.object() ) : QJsonValue( m_text() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowMappingBinding::displayValue() const
|
||||
{
|
||||
QStringList keys;
|
||||
if ( isObjectReference() )
|
||||
{
|
||||
for ( const auto& [key, reference] : references() )
|
||||
keys.append( key );
|
||||
}
|
||||
else
|
||||
{
|
||||
const QJsonValue value = toJsonValue();
|
||||
if ( !value.isObject() ) return hasValue() ? "(not a JSON object)" : "(not set)";
|
||||
keys = value.toObject().keys();
|
||||
}
|
||||
if ( keys.isEmpty() ) return "(no items)";
|
||||
|
||||
const QString count = keys.size() == 1 ? QString( "1 item" ) : QString( "%1 items" ).arg( keys.size() );
|
||||
QString names = keys.mid( 0, 3 ).join( ", " );
|
||||
if ( keys.size() > 3 ) names += ", …";
|
||||
return QString( "%1: %2" ).arg( count, names );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QList<caf::PdmOptionItemInfo> RimWorkflowMappingBinding::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions )
|
||||
{
|
||||
QList<caf::PdmOptionItemInfo> options;
|
||||
if ( fieldNeedingOptions == &m_cases ) RimTools::eclipseCaseOptionItems( &options );
|
||||
if ( fieldNeedingOptions == &m_wellPaths ) RimTools::wellPathOptionItems( &options );
|
||||
if ( fieldNeedingOptions == &m_views )
|
||||
{
|
||||
for ( RimEclipseCase* eclipseCase : RimEclipseCaseTools::eclipseCases() )
|
||||
{
|
||||
if ( !eclipseCase ) continue;
|
||||
for ( RimEclipseView* view : eclipseCase->reservoirViews() )
|
||||
{
|
||||
if ( view )
|
||||
options.push_back(
|
||||
caf::PdmOptionItemInfo( QString( "%1 / %2" ).arg( eclipseCase->caseUserDescription(), view->name() ), view ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowMappingBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
{
|
||||
m_valueType = fieldSchema.value( "value_resinsight_type" ).toString();
|
||||
RimWorkflowFieldBinding::applySchema( fieldSchema );
|
||||
|
||||
const QJsonObject mapping = fieldSchema.value( "default" ).toObject();
|
||||
if ( !isObjectReference() )
|
||||
{
|
||||
if ( fieldSchema.value( "default" ).isObject() )
|
||||
m_text = QString::fromUtf8( QJsonDocument( mapping ).toJson( QJsonDocument::Compact ) );
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore the objects of a mapping of references
|
||||
QSet<int> caseIds;
|
||||
QSet<int> viewIds;
|
||||
QSet<QString> wellPathNames;
|
||||
for ( const QJsonValue& value : mapping )
|
||||
{
|
||||
const QJsonObject reference = value.toObject();
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() == "EclipseCase" )
|
||||
caseIds.insert( reference.value( "case_id" ).toInt( -1 ) );
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() == "View" ) viewIds.insert( reference.value( "view_id" ).toInt( -1 ) );
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() == "WellPath" )
|
||||
wellPathNames.insert( reference.value( "well_path_name" ).toString() );
|
||||
}
|
||||
if ( caseIds.isEmpty() && viewIds.isEmpty() && wellPathNames.isEmpty() ) return;
|
||||
|
||||
std::vector<RimEclipseCase*> cases;
|
||||
std::vector<RimEclipseView*> views;
|
||||
for ( RimEclipseCase* eclipseCase : RimEclipseCaseTools::eclipseCases() )
|
||||
{
|
||||
if ( !eclipseCase ) continue;
|
||||
if ( caseIds.contains( eclipseCase->caseId() ) ) cases.push_back( eclipseCase );
|
||||
for ( RimEclipseView* view : eclipseCase->reservoirViews() )
|
||||
{
|
||||
if ( view && viewIds.contains( view->id() ) ) views.push_back( view );
|
||||
}
|
||||
}
|
||||
std::vector<RimWellPath*> wellPaths;
|
||||
if ( auto* project = RimProject::current() )
|
||||
{
|
||||
for ( RimWellPath* wellPath : project->allWellPaths() )
|
||||
{
|
||||
if ( wellPath && wellPathNames.contains( wellPath->name() ) ) wellPaths.push_back( wellPath );
|
||||
}
|
||||
}
|
||||
m_cases.setValue( cases );
|
||||
m_views.setValue( views );
|
||||
m_wellPaths.setValue( wellPaths );
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowFieldBinding.h"
|
||||
|
||||
#include "cafPdmPtrArrayField.h"
|
||||
|
||||
class RimEclipseCase;
|
||||
class RimEclipseView;
|
||||
class RimWellPath;
|
||||
|
||||
//==================================================================================================
|
||||
/// The `dict[str, Value]` a mapped task runs over. ResInsight objects are picked from the project
|
||||
/// and keyed by name; other values are entered as a JSON object.
|
||||
//==================================================================================================
|
||||
class RimWorkflowMappingBinding : public RimWorkflowFieldBinding
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
|
||||
public:
|
||||
RimWorkflowMappingBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
bool isObjectReference() const override;
|
||||
QString displayValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override;
|
||||
|
||||
private:
|
||||
QList<caf::PdmOptionItemInfo> calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override;
|
||||
|
||||
std::vector<std::pair<QString, QJsonObject>> references() const;
|
||||
|
||||
caf::PdmField<QString> m_valueType;
|
||||
caf::PdmField<QString> m_text;
|
||||
caf::PdmPtrArrayField<RimEclipseCase*> m_cases;
|
||||
caf::PdmPtrArrayField<RimWellPath*> m_wellPaths;
|
||||
caf::PdmPtrArrayField<RimEclipseView*> m_views;
|
||||
};
|
||||
@@ -0,0 +1,716 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowPortCompatibility.h"
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
#include "RimWorkflowSchemaTools.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QMap>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString shortName( const QString& pythonType )
|
||||
{
|
||||
return pythonType.mid( pythonType.lastIndexOf( '.' ) + 1 );
|
||||
}
|
||||
|
||||
QStringList pythonTypes( const QJsonObject& schema )
|
||||
{
|
||||
QStringList types;
|
||||
for ( const char* key : { "x-taskmaestro-python-type", "x-ri-python-type" } )
|
||||
{
|
||||
const QString type = schema.value( key ).toString();
|
||||
if ( !type.isEmpty() ) types.append( type );
|
||||
}
|
||||
for ( const QJsonValue& base : schema.value( "x-ri-python-bases" ).toArray() )
|
||||
types.append( base.toString() );
|
||||
return types;
|
||||
}
|
||||
|
||||
QJsonArray enumValues( const QJsonObject& schema )
|
||||
{
|
||||
if ( schema.contains( "const" ) ) return QJsonArray{ schema.value( "const" ) };
|
||||
return schema.value( "enum" ).toArray();
|
||||
}
|
||||
|
||||
bool isHiddenObjectValue( const QString& name, const QJsonObject& property, const QJsonObject& rootSchema )
|
||||
{
|
||||
// ObjectModel wraps its runtime object in an opaque `value` field that is not routable on its own
|
||||
return name == "value" && RimWorkflowSchemaTools::isOpaque( property, rootSchema );
|
||||
}
|
||||
|
||||
bool isModelCompatible( const QJsonObject& produced, const QJsonObject& expected )
|
||||
{
|
||||
const QString expectedType = expected.value( "x-ri-python-type" ).toString();
|
||||
if ( !expectedType.isEmpty() && produced.contains( "x-ri-python-type" ) ) return pythonTypes( produced ).contains( expectedType );
|
||||
|
||||
if ( expected.contains( "title" ) && produced.contains( "title" ) && expected.contains( "properties" ) )
|
||||
return expected.value( "title" ) == produced.value( "title" );
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isScalarCompatible( const QJsonObject& produced, const QJsonObject& expected )
|
||||
{
|
||||
const QString producedType = produced.value( "type" ).toString();
|
||||
const QString expectedType = expected.value( "type" ).toString();
|
||||
|
||||
// bool is a subclass of int in Python, but int is not a subclass of float
|
||||
if ( producedType != expectedType && !( producedType == "boolean" && expectedType == "integer" ) ) return false;
|
||||
if ( produced.value( "format" ).toString() != expected.value( "format" ).toString() ) return false;
|
||||
|
||||
const QJsonArray expectedEnum = enumValues( expected );
|
||||
if ( expectedEnum.isEmpty() ) return true;
|
||||
const QJsonArray producedEnum = enumValues( produced );
|
||||
if ( producedEnum.isEmpty() ) return false;
|
||||
for ( const QJsonValue& value : producedEnum )
|
||||
{
|
||||
if ( !expectedEnum.contains( value ) ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<RimWorkflowPort> findPort( const std::vector<RimWorkflowPort>& ports, const QString& name )
|
||||
{
|
||||
for ( const auto& port : ports )
|
||||
{
|
||||
if ( port.name == name ) return port;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool hasRequiredConfigurableField( const QJsonObject& inputSchema, const QStringList& covered )
|
||||
{
|
||||
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
||||
for ( const QString& field : RimWorkflowSchemaTools::requiredFields( inputSchema ) )
|
||||
{
|
||||
if ( covered.contains( field ) ) continue;
|
||||
if ( RimWorkflowSchemaTools::isConfigurable( properties.value( field ).toObject(), inputSchema ) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::expected<void, QString>
|
||||
checkWholeToWhole( const QJsonObject& upstreamType, const QJsonObject& downstreamType, const QString& from, const QString& to )
|
||||
{
|
||||
const QJsonObject outputSchema = upstreamType.value( "output_schema" ).toObject();
|
||||
const QJsonObject inputSchema = downstreamType.value( "input_schema" ).toObject();
|
||||
if ( RimWorkflowPortCompatibility::isCompatible( outputSchema, outputSchema, inputSchema, inputSchema ) ) return {};
|
||||
|
||||
const QString mismatch = QString( "'%1' outputs %2, but '%3' expects %4" )
|
||||
.arg( from, RimWorkflowPortCompatibility::typeName( outputSchema, outputSchema ) )
|
||||
.arg( to, RimWorkflowPortCompatibility::typeName( inputSchema, inputSchema ) );
|
||||
|
||||
// taskmaestro merges the upstream fields with configured values when the task has config fields
|
||||
const QStringList shared = RimWorkflowPortCompatibility::coveredByWholeOutput( upstreamType, downstreamType );
|
||||
if ( shared.isEmpty() || !hasRequiredConfigurableField( inputSchema, shared ) ) return std::unexpected( mismatch );
|
||||
|
||||
const QJsonObject outputProperties = outputSchema.value( "properties" ).toObject();
|
||||
const QJsonObject inputProperties = inputSchema.value( "properties" ).toObject();
|
||||
for ( const QString& field : shared )
|
||||
{
|
||||
if ( !RimWorkflowPortCompatibility::isCompatible( outputProperties.value( field ).toObject(),
|
||||
outputSchema,
|
||||
inputProperties.value( field ).toObject(),
|
||||
inputSchema ) )
|
||||
{
|
||||
return std::unexpected( QString( "Type mismatch for field '%1': %2" ).arg( field, mismatch ) );
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Icon of the ResInsight object a schema holds, or an empty string. The rips class is found in the
|
||||
/// Python class bases: directly for opaque rips objects, or as `ObjectModel[Class]` for wrappers.
|
||||
/// The icons are the ones the matching ResInsight project classes use.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString objectIconResource( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
|
||||
{
|
||||
const QString icon = objectIconResource( option.toObject(), rootSchema );
|
||||
if ( !icon.isEmpty() ) return icon;
|
||||
}
|
||||
}
|
||||
if ( resolved.value( "type" ).toString() == "array" ) return objectIconResource( resolved.value( "items" ).toObject(), rootSchema );
|
||||
if ( resolved.value( "type" ).toString() == "object" && resolved.value( "additionalProperties" ).isObject() &&
|
||||
!resolved.contains( "properties" ) )
|
||||
return objectIconResource( resolved.value( "additionalProperties" ).toObject(), rootSchema );
|
||||
|
||||
static const QMap<QString, QString> icons = { { "Instance", ":/AppLogo48x48.png" },
|
||||
{ "Case", ":/Case48x48.png" },
|
||||
{ "Reservoir", ":/Case48x48.png" },
|
||||
{ "EclipseCase", ":/Case48x48.png" },
|
||||
{ "View", ":/3DView16x16.png" },
|
||||
{ "EclipseView", ":/3DView16x16.png" },
|
||||
{ "WellPath", ":/Well.svg" },
|
||||
{ "Surface", ":/ReservoirSurface16x16.png" } };
|
||||
|
||||
QJsonArray bases = resolved.value( "x-ri-python-bases" ).toArray();
|
||||
if ( bases.isEmpty() && resolved.contains( "x-ri-python-type" ) ) bases.append( resolved.value( "x-ri-python-type" ) );
|
||||
|
||||
static const QRegularExpression objectModel( R"(ObjectModel\[(\w+)\]$)" );
|
||||
for ( const QJsonValue& value : bases )
|
||||
{
|
||||
const QString base = value.toString();
|
||||
QString className;
|
||||
if ( const auto match = objectModel.match( base ); match.hasMatch() )
|
||||
className = match.captured( 1 );
|
||||
else if ( base.startsWith( "rips." ) )
|
||||
className = base.section( '.', -1 );
|
||||
if ( icons.contains( className ) ) return icons.value( className );
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Icon of a plain value (string, number, bool, date, path), or an empty string. The icons are Codicons,
|
||||
/// see Resources/codicons/README.md.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString valueIconResource( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
|
||||
{
|
||||
const QString icon = valueIconResource( option.toObject(), rootSchema );
|
||||
if ( !icon.isEmpty() ) return icon;
|
||||
}
|
||||
}
|
||||
|
||||
const QString type = resolved.value( "type" ).toString();
|
||||
if ( type == "array" ) return valueIconResource( resolved.value( "items" ).toObject(), rootSchema );
|
||||
if ( type == "object" && resolved.value( "additionalProperties" ).isObject() && !resolved.contains( "properties" ) )
|
||||
return valueIconResource( resolved.value( "additionalProperties" ).toObject(), rootSchema );
|
||||
|
||||
if ( type == "boolean" ) return ":/codicons/check.svg";
|
||||
if ( type == "integer" || type == "number" ) return ":/codicons/symbol-numeric.svg";
|
||||
if ( type != "string" ) return {};
|
||||
|
||||
const QString format = resolved.value( "format" ).toString();
|
||||
if ( format == "date" || format == "date-time" ) return ":/codicons/calendar.svg";
|
||||
if ( format == "directory-path" ) return ":/codicons/folder.svg";
|
||||
if ( format == "path" || format == "file-path" ) return ":/codicons/file.svg";
|
||||
return ":/codicons/symbol-string.svg";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowPort::isWhole() const
|
||||
{
|
||||
return name.isEmpty();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The whole input model first, then its fields
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::inputPorts( const QJsonObject& taskType )
|
||||
{
|
||||
const QJsonObject inputSchema = taskType.value( "input_schema" ).toObject();
|
||||
|
||||
std::vector<RimWorkflowPort> ports;
|
||||
ports.push_back( { .name = "",
|
||||
.schema = inputSchema,
|
||||
.rootSchema = inputSchema,
|
||||
.description = inputSchema.value( "description" ).toString(),
|
||||
.typeName = typeName( inputSchema, inputSchema ),
|
||||
.iconResource = iconResource( inputSchema, inputSchema ),
|
||||
.required = true } );
|
||||
|
||||
const QStringList required = RimWorkflowSchemaTools::requiredFields( inputSchema );
|
||||
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
||||
for ( const QString& name : RimWorkflowSchemaTools::propertyNames( inputSchema ) )
|
||||
{
|
||||
const QJsonObject property = properties.value( name ).toObject();
|
||||
if ( isHiddenObjectValue( name, property, inputSchema ) ) continue;
|
||||
ports.push_back( { .name = name,
|
||||
.schema = property,
|
||||
.rootSchema = inputSchema,
|
||||
.description = property.value( "description" ).toString(),
|
||||
.typeName = typeName( property, inputSchema ),
|
||||
.iconResource = iconResource( property, inputSchema ),
|
||||
.required = required.contains( name ),
|
||||
.configurable = RimWorkflowSchemaTools::isConfigurable( property, inputSchema ) } );
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The whole output model first, then its fields
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<RimWorkflowPort> RimWorkflowPortCompatibility::outputPorts( const QJsonObject& taskType )
|
||||
{
|
||||
const QJsonObject outputSchema = taskType.value( "output_schema" ).toObject();
|
||||
|
||||
std::vector<RimWorkflowPort> ports;
|
||||
ports.push_back( { .name = "",
|
||||
.schema = outputSchema,
|
||||
.rootSchema = outputSchema,
|
||||
.description = outputSchema.value( "description" ).toString(),
|
||||
.typeName = typeName( outputSchema, outputSchema ),
|
||||
.iconResource = iconResource( outputSchema, outputSchema ) } );
|
||||
|
||||
const QJsonObject properties = outputSchema.value( "properties" ).toObject();
|
||||
for ( const QString& name : RimWorkflowSchemaTools::propertyNames( outputSchema ) )
|
||||
{
|
||||
const QJsonObject property = properties.value( name ).toObject();
|
||||
if ( isHiddenObjectValue( name, property, outputSchema ) ) continue;
|
||||
ports.push_back( { .name = name,
|
||||
.schema = property,
|
||||
.rootSchema = outputSchema,
|
||||
.description = property.value( "description" ).toString(),
|
||||
.typeName = typeName( property, outputSchema ),
|
||||
.iconResource = iconResource( property, outputSchema ) } );
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<RimWorkflowPort> RimWorkflowPortCompatibility::inputPort( const QJsonObject& taskType, const QString& name )
|
||||
{
|
||||
return findPort( inputPorts( taskType ), name );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<RimWorkflowPort> RimWorkflowPortCompatibility::outputPort( const QJsonObject& taskType, const QString& name )
|
||||
{
|
||||
return findPort( outputPorts( taskType ), name );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A schema without any type information accepts and produces anything
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowPortCompatibility::isAny( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
||||
for ( const char* key : { "type", "anyOf", "oneOf", "properties", "enum", "const", "x-taskmaestro-opaque", "$ref" } )
|
||||
{
|
||||
if ( resolved.contains( key ) ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Whether every value of the produced schema is accepted by the expected schema. Models and
|
||||
/// runtime objects compare their Python class and its bases, like `issubclass`.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowPortCompatibility::isCompatible( const QJsonObject& produced,
|
||||
const QJsonObject& producedRoot,
|
||||
const QJsonObject& expected,
|
||||
const QJsonObject& expectedRoot )
|
||||
{
|
||||
if ( isAny( produced, producedRoot ) || isAny( expected, expectedRoot ) ) return true;
|
||||
|
||||
const QJsonObject p = RimWorkflowSchemaTools::resolveReferences( produced, producedRoot );
|
||||
const QJsonObject e = RimWorkflowSchemaTools::resolveReferences( expected, expectedRoot );
|
||||
|
||||
// Every produced option must be accepted; an expected union needs one accepting option
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
if ( !p.value( unionKey ).isArray() ) continue;
|
||||
for ( const QJsonValue& option : p.value( unionKey ).toArray() )
|
||||
{
|
||||
if ( !isCompatible( option.toObject(), producedRoot, e, expectedRoot ) ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
if ( !e.value( unionKey ).isArray() ) continue;
|
||||
for ( const QJsonValue& option : e.value( unionKey ).toArray() )
|
||||
{
|
||||
if ( isCompatible( p, producedRoot, option.toObject(), expectedRoot ) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool producedOpaque = p.value( "x-taskmaestro-opaque" ).toBool( false );
|
||||
if ( e.value( "x-taskmaestro-opaque" ).toBool( false ) )
|
||||
{
|
||||
return producedOpaque && pythonTypes( p ).contains( e.value( "x-taskmaestro-python-type" ).toString() );
|
||||
}
|
||||
if ( producedOpaque ) return false;
|
||||
|
||||
const QString producedType = p.value( "type" ).toString();
|
||||
const QString expectedType = e.value( "type" ).toString();
|
||||
if ( producedType == "object" || expectedType == "object" )
|
||||
{
|
||||
if ( producedType != expectedType ) return false;
|
||||
if ( e.value( "additionalProperties" ).isObject() && !e.contains( "properties" ) )
|
||||
{
|
||||
return isCompatible( p.value( "additionalProperties" ).toObject(),
|
||||
producedRoot,
|
||||
e.value( "additionalProperties" ).toObject(),
|
||||
expectedRoot );
|
||||
}
|
||||
return isModelCompatible( p, e );
|
||||
}
|
||||
|
||||
if ( producedType == "array" || expectedType == "array" )
|
||||
{
|
||||
if ( producedType != expectedType ) return false;
|
||||
return isCompatible( p.value( "items" ).toObject(), producedRoot, e.value( "items" ).toObject(), expectedRoot );
|
||||
}
|
||||
|
||||
return isScalarCompatible( p, e );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowPortCompatibility::isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected )
|
||||
{
|
||||
return isCompatible( produced.schema, produced.rootSchema, expected.schema, expected.rootSchema );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Short readable type name used in tooltips and error messages
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowPortCompatibility::typeName( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
if ( isAny( schema, rootSchema ) ) return "Any";
|
||||
|
||||
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( schema, rootSchema );
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
if ( !resolved.value( unionKey ).isArray() ) continue;
|
||||
QStringList names;
|
||||
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
|
||||
names.append( typeName( option.toObject(), rootSchema ) );
|
||||
return names.join( " | " );
|
||||
}
|
||||
|
||||
if ( resolved.value( "x-taskmaestro-opaque" ).toBool( false ) )
|
||||
return shortName( resolved.value( "x-taskmaestro-python-type" ).toString( "object" ) );
|
||||
if ( resolved.contains( "x-ri-python-type" ) ) return shortName( resolved.value( "x-ri-python-type" ).toString() );
|
||||
|
||||
const QString type = resolved.value( "type" ).toString();
|
||||
if ( type == "object" && resolved.value( "additionalProperties" ).isObject() && !resolved.contains( "properties" ) )
|
||||
return QString( "dict[str, %1]" ).arg( typeName( resolved.value( "additionalProperties" ).toObject(), rootSchema ) );
|
||||
if ( type == "object" ) return resolved.value( "title" ).toString( "object" );
|
||||
if ( type == "array" ) return QString( "list[%1]" ).arg( typeName( resolved.value( "items" ).toObject(), rootSchema ) );
|
||||
|
||||
static const QMap<QString, QString> pythonNames = { { "string", "str" },
|
||||
{ "integer", "int" },
|
||||
{ "number", "float" },
|
||||
{ "boolean", "bool" },
|
||||
{ "null", "None" } };
|
||||
const QString format = resolved.value( "format" ).toString();
|
||||
if ( format == "date" ) return "date";
|
||||
if ( format == "date-time" ) return "datetime";
|
||||
if ( format == "path" || format == "file-path" || format == "directory-path" ) return "Path";
|
||||
return pythonNames.value( type, type );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Icon of a port: the ResInsight object it holds, otherwise the kind of value
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowPortCompatibility::iconResource( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QString objectIcon = objectIconResource( schema, rootSchema );
|
||||
return objectIcon.isEmpty() ? valueIconResource( schema, rootSchema ) : objectIcon;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Port name -> icon resource, for the ports that have an icon
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowPortCompatibility::portIcons( const std::vector<RimWorkflowPort>& ports )
|
||||
{
|
||||
QJsonObject icons;
|
||||
for ( const auto& port : ports )
|
||||
{
|
||||
if ( !port.iconResource.isEmpty() ) icons[port.name] = port.iconResource;
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Port name -> type name, with an empty name for the whole model. Used to label ports in the graph.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowPortCompatibility::portTypes( const std::vector<RimWorkflowPort>& ports )
|
||||
{
|
||||
QJsonObject types;
|
||||
for ( const auto& port : ports )
|
||||
types[port.name] = port.typeName;
|
||||
return types;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Input fields of the downstream task supplied when it takes the whole upstream output
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowPortCompatibility::coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType )
|
||||
{
|
||||
const QJsonObject outputSchema = upstreamTaskType.value( "output_schema" ).toObject();
|
||||
const QJsonObject inputSchema = downstreamTaskType.value( "input_schema" ).toObject();
|
||||
const QStringList inputFields = RimWorkflowSchemaTools::propertyNames( inputSchema );
|
||||
if ( isCompatible( outputSchema, outputSchema, inputSchema, inputSchema ) ) return inputFields;
|
||||
|
||||
const QStringList outputFields = RimWorkflowSchemaTools::propertyNames( outputSchema );
|
||||
QStringList covered;
|
||||
for ( const QString& field : inputFields )
|
||||
{
|
||||
if ( outputFields.contains( field ) ) covered.append( field );
|
||||
}
|
||||
return covered;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Whether adding the connection from -> to closes a cycle
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowPortCompatibility::wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to )
|
||||
{
|
||||
if ( from == to ) return true;
|
||||
|
||||
QStringList pending{ to };
|
||||
QSet<QString> visited;
|
||||
while ( !pending.isEmpty() )
|
||||
{
|
||||
const QString current = pending.takeLast();
|
||||
if ( current == from ) return true;
|
||||
if ( visited.contains( current ) ) continue;
|
||||
visited.insert( current );
|
||||
for ( const auto& edge : definition.edges )
|
||||
{
|
||||
if ( edge.from == current ) pending.append( edge.to );
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A mapped task runs once per item: the key and value fields are filled by the map and are not
|
||||
/// inputs, and the output is `dict[str, Output]`
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowPortCompatibility::mappedTaskType( const QJsonObject& taskType, const RimWorkflowTaskMap& map )
|
||||
{
|
||||
QJsonObject inputSchema = taskType.value( "input_schema" ).toObject();
|
||||
QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
||||
properties.remove( map.keyAs );
|
||||
properties.remove( map.valueAs );
|
||||
inputSchema["properties"] = properties;
|
||||
|
||||
QJsonArray required;
|
||||
for ( const QJsonValue& field : inputSchema.value( "required" ).toArray() )
|
||||
{
|
||||
if ( field.toString() != map.keyAs && field.toString() != map.valueAs ) required.append( field );
|
||||
}
|
||||
inputSchema["required"] = required;
|
||||
|
||||
QJsonObject result = taskType;
|
||||
result["input_schema"] = inputSchema;
|
||||
result["output_schema"] = mappedOutputSchema( taskType.value( "output_schema" ).toObject() );
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// `MappedOutput[Output]`, a `dict[str, Output]`. The output model moves into `$defs`, so its own
|
||||
/// references still resolve against the new root.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowPortCompatibility::mappedOutputSchema( const QJsonObject& outputSchema )
|
||||
{
|
||||
QJsonObject definitions = outputSchema.value( "$defs" ).toObject();
|
||||
QJsonObject item = outputSchema;
|
||||
item.remove( "$defs" );
|
||||
|
||||
QString itemKey = "MappedItem";
|
||||
while ( definitions.contains( itemKey ) )
|
||||
itemKey += "_";
|
||||
definitions[itemKey] = item;
|
||||
|
||||
return QJsonObject{ { "type", "object" },
|
||||
{ "title", QString( "dict[str, %1]" ).arg( typeName( outputSchema, outputSchema ) ) },
|
||||
{ "additionalProperties", QJsonObject{ { "$ref", "#/$defs/" + itemKey } } },
|
||||
{ "$defs", definitions },
|
||||
{ "x-ri-mapped-output", true } };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<std::pair<RimWorkflowDefinitionEdge::Collect, QJsonObject>>
|
||||
RimWorkflowPortCompatibility::collectTarget( const RimWorkflowPort& expected )
|
||||
{
|
||||
if ( expected.isWhole() ) return std::nullopt;
|
||||
|
||||
const QJsonObject resolved = RimWorkflowSchemaTools::resolveReferences( expected.schema, expected.rootSchema );
|
||||
QJsonArray options;
|
||||
for ( const char* unionKey : { "anyOf", "oneOf" } )
|
||||
{
|
||||
for ( const QJsonValue& option : resolved.value( unionKey ).toArray() )
|
||||
options.append( option );
|
||||
}
|
||||
if ( options.isEmpty() ) options.append( resolved );
|
||||
|
||||
for ( const QJsonValue& value : options )
|
||||
{
|
||||
const QJsonObject option = RimWorkflowSchemaTools::resolveReferences( value.toObject(), expected.rootSchema );
|
||||
const QString type = option.value( "type" ).toString();
|
||||
if ( type == "array" && option.value( "items" ).isObject() )
|
||||
return std::make_pair( RimWorkflowDefinitionEdge::Collect::List, option.value( "items" ).toObject() );
|
||||
if ( type == "object" && option.value( "additionalProperties" ).isObject() && !option.contains( "properties" ) )
|
||||
return std::make_pair( RimWorkflowDefinitionEdge::Collect::Dict, option.value( "additionalProperties" ).toObject() );
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Checks a new connection and returns the edge to add
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<RimWorkflowDefinitionEdge, QString> RimWorkflowPortCompatibility::resolveConnection( const RimWorkflowDefinition& definition,
|
||||
const QString& from,
|
||||
const QString& output,
|
||||
const QString& to,
|
||||
const QString& input )
|
||||
{
|
||||
using Collect = RimWorkflowDefinitionEdge::Collect;
|
||||
|
||||
if ( from == to ) return std::unexpected( "A task cannot be connected to itself" );
|
||||
if ( !definition.findNode( from ) ) return std::unexpected( QString( "Unknown task '%1'" ).arg( from ) );
|
||||
const auto* downstream = definition.findNode( to );
|
||||
if ( !downstream ) return std::unexpected( QString( "Unknown task '%1'" ).arg( to ) );
|
||||
|
||||
const QJsonObject upstreamType = definition.taskTypeForNode( from );
|
||||
const QJsonObject downstreamType = definition.taskTypeForNode( to );
|
||||
if ( upstreamType.isEmpty() ) return std::unexpected( QString( "The task type of '%1' is not installed" ).arg( from ) );
|
||||
if ( downstreamType.isEmpty() ) return std::unexpected( QString( "The task type of '%1' is not installed" ).arg( to ) );
|
||||
|
||||
const auto producedPort = outputPort( upstreamType, output );
|
||||
if ( !producedPort ) return std::unexpected( QString( "'%1' has no output '%2'" ).arg( from, output ) );
|
||||
const auto expectedPort = inputPort( downstreamType, input );
|
||||
if ( !expectedPort ) return std::unexpected( QString( "'%1' has no input '%2'" ).arg( to, input ) );
|
||||
if ( downstream->map && input.isEmpty() )
|
||||
return std::unexpected( QString( "'%1' is mapped and takes its inputs field by field" ).arg( to ) );
|
||||
|
||||
RimWorkflowDefinitionEdge edge{ .from = from, .output = output, .to = to, .input = input };
|
||||
if ( !input.isEmpty() && !isCompatible( *producedPort, *expectedPort ) )
|
||||
{
|
||||
const auto target = collectTarget( *expectedPort );
|
||||
if ( target && isCompatible( producedPort->schema, producedPort->rootSchema, target->second, expectedPort->rootSchema ) )
|
||||
{
|
||||
edge.collect = target->first;
|
||||
}
|
||||
else
|
||||
{
|
||||
const QString source = output.isEmpty() ? from : from + "." + output;
|
||||
return std::unexpected(
|
||||
QString( "%1 is %2, but %3.%4 expects %5" ).arg( source, producedPort->typeName, to, input, expectedPort->typeName ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Edges into the same input are replaced by the new connection, except other collected members
|
||||
RimWorkflowDefinition remaining = definition;
|
||||
std::erase_if( remaining.edges,
|
||||
[&]( const RimWorkflowDefinitionEdge& existing )
|
||||
{ return existing.to == to && existing.input == input && ( !edge.isCollected() || existing.collect != edge.collect ); } );
|
||||
if ( wouldCreateCycle( remaining, from, to ) ) return std::unexpected( "The connection would create a cycle" );
|
||||
|
||||
QStringList keys;
|
||||
for ( const auto& existing : remaining.incomingEdges( to ) )
|
||||
{
|
||||
if ( input.isEmpty() && !existing.isWholeInput() )
|
||||
return std::unexpected( QString( "'%1' already has individual inputs connected and cannot also take a whole input" ).arg( to ) );
|
||||
if ( !input.isEmpty() && existing.isWholeInput() )
|
||||
return std::unexpected( QString( "'%1' takes its whole input from '%2'; disconnect it first" ).arg( to, existing.from ) );
|
||||
if ( existing.input != input ) continue;
|
||||
if ( existing.sameConnection( edge ) )
|
||||
return std::unexpected( QString( "%1 is already collected into %2.%3" ).arg( from, to, input ) );
|
||||
keys.append( existing.key );
|
||||
}
|
||||
|
||||
if ( input.isEmpty() && output.isEmpty() )
|
||||
{
|
||||
if ( auto check = checkWholeToWhole( upstreamType, downstreamType, from, to ); !check ) return std::unexpected( check.error() );
|
||||
return edge;
|
||||
}
|
||||
if ( input.isEmpty() && !isCompatible( *producedPort, *expectedPort ) )
|
||||
{
|
||||
return std::unexpected(
|
||||
QString( "%1.%2 is %3, but %4 expects %5" ).arg( from, output, producedPort->typeName, to, expectedPort->typeName ) );
|
||||
}
|
||||
|
||||
if ( edge.collect == Collect::Dict )
|
||||
{
|
||||
// The upstream task name is the default key
|
||||
edge.key = output.isEmpty() ? from : from + "_" + output;
|
||||
for ( int index = 2; keys.contains( edge.key ); ++index )
|
||||
edge.key = QString( "%1_%2" ).arg( output.isEmpty() ? from : from + "_" + output ).arg( index );
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflowPortCompatibility::canConnect( const RimWorkflowDefinition& definition,
|
||||
const QString& from,
|
||||
const QString& output,
|
||||
const QString& to,
|
||||
const QString& input )
|
||||
{
|
||||
auto edge = resolveConnection( definition, from, output, to, input );
|
||||
if ( !edge ) return std::unexpected( edge.error() );
|
||||
return {};
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Type check of one member of a collected input
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<void, QString> RimWorkflowPortCompatibility::checkCollectMember( const RimWorkflowDefinition& definition,
|
||||
const RimWorkflowDefinitionEdge& edge )
|
||||
{
|
||||
const QJsonObject upstreamType = definition.taskTypeForNode( edge.from );
|
||||
const QJsonObject downstreamType = definition.taskTypeForNode( edge.to );
|
||||
if ( upstreamType.isEmpty() || downstreamType.isEmpty() ) return {};
|
||||
|
||||
const auto producedPort = outputPort( upstreamType, edge.output );
|
||||
if ( !producedPort ) return std::unexpected( QString( "'%1' has no output '%2'" ).arg( edge.from, edge.output ) );
|
||||
const auto expectedPort = inputPort( downstreamType, edge.input );
|
||||
if ( !expectedPort || expectedPort->isWhole() )
|
||||
return std::unexpected( QString( "'%1' has no input '%2'" ).arg( edge.to, edge.input ) );
|
||||
|
||||
const auto target = collectTarget( *expectedPort );
|
||||
const bool isList = edge.collect == RimWorkflowDefinitionEdge::Collect::List;
|
||||
if ( !target || target->first != edge.collect )
|
||||
{
|
||||
return std::unexpected( QString( "Input '%1' is %2 and cannot collect %3" )
|
||||
.arg( edge.input, expectedPort->typeName, isList ? "a list of members" : "keyed members" ) );
|
||||
}
|
||||
if ( !isCompatible( producedPort->schema, producedPort->rootSchema, target->second, expectedPort->rootSchema ) )
|
||||
{
|
||||
const QString source = edge.output.isEmpty() ? edge.from : edge.from + "." + edge.output;
|
||||
return std::unexpected(
|
||||
QString( "%1 is %2, but the members of %3.%4 must be %5" )
|
||||
.arg( source, producedPort->typeName, edge.to, edge.input, typeName( target->second, expectedPort->rootSchema ) ) );
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
//==================================================================================================
|
||||
/// A connectable input or output of a task. An empty name is the whole input or output model.
|
||||
//==================================================================================================
|
||||
struct RimWorkflowPort
|
||||
{
|
||||
QString name{};
|
||||
QJsonObject schema{};
|
||||
QJsonObject rootSchema{};
|
||||
QString description{};
|
||||
QString typeName{};
|
||||
QString iconResource{};
|
||||
bool required = true;
|
||||
bool configurable = false;
|
||||
|
||||
bool isWhole() const;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
/// Static type checks for workflow connections. These mirror taskmaestro's `_is_type_compatible`
|
||||
/// on the JSON schemas; `taskmaestro workflow describe` has the final say.
|
||||
//==================================================================================================
|
||||
namespace RimWorkflowPortCompatibility
|
||||
{
|
||||
std::vector<RimWorkflowPort> inputPorts( const QJsonObject& taskType );
|
||||
std::vector<RimWorkflowPort> outputPorts( const QJsonObject& taskType );
|
||||
std::optional<RimWorkflowPort> inputPort( const QJsonObject& taskType, const QString& name );
|
||||
std::optional<RimWorkflowPort> outputPort( const QJsonObject& taskType, const QString& name );
|
||||
|
||||
bool isCompatible( const QJsonObject& produced, const QJsonObject& producedRoot, const QJsonObject& expected, const QJsonObject& expectedRoot );
|
||||
bool isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected );
|
||||
bool isAny( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QString typeName( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QString iconResource( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QJsonObject portTypes( const std::vector<RimWorkflowPort>& ports );
|
||||
QJsonObject portIcons( const std::vector<RimWorkflowPort>& ports );
|
||||
|
||||
QStringList coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType );
|
||||
bool wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to );
|
||||
|
||||
// Mapped tasks
|
||||
QJsonObject mappedTaskType( const QJsonObject& taskType, const RimWorkflowTaskMap& map );
|
||||
QJsonObject mappedOutputSchema( const QJsonObject& outputSchema );
|
||||
|
||||
// Collected inputs: the kind and element schema of a `list[T]` or `dict[str, T]` input
|
||||
std::optional<std::pair<RimWorkflowDefinitionEdge::Collect, QJsonObject>> collectTarget( const RimWorkflowPort& expected );
|
||||
|
||||
// The edge to add for a new connection, collecting into a list or dict input when the upstream
|
||||
// value is an element of it. An existing connection to the same input is replaced, except for
|
||||
// other members of the same collection.
|
||||
std::expected<RimWorkflowDefinitionEdge, QString> resolveConnection( const RimWorkflowDefinition& definition,
|
||||
const QString& from,
|
||||
const QString& output,
|
||||
const QString& to,
|
||||
const QString& input );
|
||||
std::expected<void, QString>
|
||||
canConnect( const RimWorkflowDefinition& definition, const QString& from, const QString& output, const QString& to, const QString& input );
|
||||
std::expected<void, QString> checkCollectMember( const RimWorkflowDefinition& definition, const RimWorkflowDefinitionEdge& edge );
|
||||
} // namespace RimWorkflowPortCompatibility
|
||||
@@ -0,0 +1,265 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowSchemaTools.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QMap>
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowSchemaTools::resolveRef( const QString& ref, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QString prefix = "#/$defs/";
|
||||
if ( !ref.startsWith( prefix ) ) return {};
|
||||
return rootSchema.value( "$defs" ).toObject().value( ref.mid( prefix.size() ) ).toObject();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Follow $ref and unwrap single-item allOf, keeping the sibling keywords of a $ref (description, default)
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowSchemaTools::resolveReferences( QJsonObject schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
constexpr int maxDepth = 16;
|
||||
for ( int depth = 0; depth < maxDepth; ++depth )
|
||||
{
|
||||
if ( schema.contains( "$ref" ) )
|
||||
{
|
||||
QJsonObject target = resolveRef( schema.value( "$ref" ).toString(), rootSchema );
|
||||
schema.remove( "$ref" );
|
||||
for ( auto it = schema.begin(); it != schema.end(); ++it )
|
||||
{
|
||||
if ( !target.contains( it.key() ) ) target.insert( it.key(), it.value() );
|
||||
}
|
||||
schema = target;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray allOf = schema.value( "allOf" ).toArray();
|
||||
if ( allOf.size() == 1 )
|
||||
{
|
||||
schema = allOf.first().toObject();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Follow $ref and unwrap single-item allOf and Optional (anyOf with null) to the underlying type
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowSchemaTools::underlyingSchema( QJsonObject schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
constexpr int maxDepth = 16;
|
||||
for ( int depth = 0; depth < maxDepth; ++depth )
|
||||
{
|
||||
if ( schema.contains( "$ref" ) )
|
||||
{
|
||||
schema = resolveRef( schema.value( "$ref" ).toString(), rootSchema );
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray allOf = schema.value( "allOf" ).toArray();
|
||||
if ( allOf.size() == 1 )
|
||||
{
|
||||
schema = allOf.first().toObject();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( schema.value( "anyOf" ).isArray() )
|
||||
{
|
||||
QJsonArray nonNull;
|
||||
for ( const QJsonValue& arm : schema.value( "anyOf" ).toArray() )
|
||||
{
|
||||
if ( arm.toObject().value( "type" ).toString() != "null" ) nonNull.append( arm );
|
||||
}
|
||||
if ( nonNull.size() == 1 )
|
||||
{
|
||||
schema = nonNull.first().toObject();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowSchemaTools::isOpaque( const QJsonObject& schema, const QJsonObject& rootSchema )
|
||||
{
|
||||
return underlyingSchema( schema, rootSchema ).value( "x-taskmaestro-opaque" ).toBool( false );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Python type of a runtime-only object, either bare or wrapped in an ObjectModel `value` field
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowSchemaTools::objectPythonType( const QJsonObject& typeSchema, const QJsonObject& rootSchema )
|
||||
{
|
||||
if ( typeSchema.contains( "x-taskmaestro-python-type" ) ) return typeSchema.value( "x-taskmaestro-python-type" ).toString();
|
||||
|
||||
const QJsonObject valueSchema = typeSchema.value( "properties" ).toObject().value( "value" ).toObject();
|
||||
if ( valueSchema.isEmpty() ) return {};
|
||||
return underlyingSchema( valueSchema, rootSchema ).value( "x-taskmaestro-python-type" ).toString();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowSchemaTools::jsonType( const QJsonObject& typeSchema )
|
||||
{
|
||||
const QString type = typeSchema.value( "type" ).toString();
|
||||
if ( type == "boolean" || type == "integer" || type == "number" || type == "array" ) return type;
|
||||
return "string";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// taskmaestro_resinsight.models.Vec3 is a plain pydantic model (x, y, z numbers), so it has no
|
||||
/// x-taskmaestro-python-type tag; recognize it structurally instead.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowSchemaTools::isVec3Schema( const QJsonObject& typeSchema )
|
||||
{
|
||||
if ( typeSchema.value( "type" ).toString() != "object" ) return false;
|
||||
|
||||
const QJsonObject properties = typeSchema.value( "properties" ).toObject();
|
||||
if ( properties.size() != 3 ) return false;
|
||||
|
||||
for ( const QString& component : QStringList{ "x", "y", "z" } )
|
||||
{
|
||||
if ( properties.value( component ).toObject().value( "type" ).toString() != "number" ) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Map a Python class path such as `rips.generated.generated_classes.WellPath` to the ResInsight
|
||||
/// object type used to select the field binding. Returns an empty string for unsupported types.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowSchemaTools::resinsightTypeFromPythonType( const QString& pythonType )
|
||||
{
|
||||
const int separator = pythonType.lastIndexOf( '.' );
|
||||
if ( separator < 0 ) return {};
|
||||
|
||||
const QString module = pythonType.left( separator );
|
||||
if ( module != "rips" && !module.startsWith( "rips." ) ) return {};
|
||||
|
||||
static const QMap<QString, QString> typeMap = { { "Case", "EclipseCase" },
|
||||
{ "Reservoir", "EclipseCase" },
|
||||
{ "EclipseCase", "EclipseCase" },
|
||||
{ "WellPath", "WellPath" },
|
||||
{ "View", "View" },
|
||||
{ "EclipseView", "View" } };
|
||||
return typeMap.value( pythonType.mid( separator + 1 ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowSchemaTools::requiredFields( const QJsonObject& modelSchema )
|
||||
{
|
||||
QStringList fields;
|
||||
for ( const QJsonValue& value : modelSchema.value( "required" ).toArray() )
|
||||
fields.append( value.toString() );
|
||||
return fields;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Property names in schema order
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowSchemaTools::propertyNames( const QJsonObject& modelSchema )
|
||||
{
|
||||
// QJsonObject sorts its keys; the `required` list and the original document keep model order
|
||||
QStringList names;
|
||||
const QJsonObject properties = modelSchema.value( "properties" ).toObject();
|
||||
for ( const QString& name : requiredFields( modelSchema ) )
|
||||
{
|
||||
if ( properties.contains( name ) && !names.contains( name ) ) names.append( name );
|
||||
}
|
||||
for ( const QString& name : properties.keys() )
|
||||
{
|
||||
if ( !names.contains( name ) ) names.append( name );
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Whether a value for the input property can be entered in ResInsight (a literal or an object reference)
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowSchemaTools::isConfigurable( const QJsonObject& property, const QJsonObject& rootSchema )
|
||||
{
|
||||
const QJsonObject typeSchema = underlyingSchema( property, rootSchema );
|
||||
const QString pythonType = objectPythonType( typeSchema, rootSchema );
|
||||
if ( !pythonType.isEmpty() )
|
||||
{
|
||||
// An object model with more required fields than `value` cannot be given as a single object
|
||||
QStringList required = requiredFields( typeSchema );
|
||||
required.removeAll( "value" );
|
||||
return required.isEmpty() && !resinsightTypeFromPythonType( pythonType ).isEmpty();
|
||||
}
|
||||
if ( typeSchema.value( "x-taskmaestro-opaque" ).toBool( false ) ) return false;
|
||||
if ( isVec3Schema( typeSchema ) ) return true;
|
||||
return typeSchema.value( "type" ).toString() != "object";
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowSchemaTools::configFieldSchema( const QString& fieldName,
|
||||
const QJsonObject& inputSchema,
|
||||
const QStringList& requiredFields,
|
||||
const QJsonObject& configValues )
|
||||
{
|
||||
QJsonObject entry{ { "name", fieldName } };
|
||||
|
||||
const QJsonObject properties = inputSchema.value( "properties" ).toObject();
|
||||
if ( !properties.contains( fieldName ) )
|
||||
{
|
||||
entry["type"] = "string";
|
||||
entry["required"] = true;
|
||||
entry["error"] = "field not found in input model";
|
||||
return entry;
|
||||
}
|
||||
|
||||
const QJsonObject property = properties.value( fieldName ).toObject();
|
||||
const QJsonObject typeSchema = underlyingSchema( property, inputSchema );
|
||||
QString resinsightType = resinsightTypeFromPythonType( objectPythonType( typeSchema, inputSchema ) );
|
||||
if ( resinsightType.isEmpty() && isVec3Schema( typeSchema ) ) resinsightType = "Vec3";
|
||||
|
||||
entry["type"] = resinsightType.isEmpty() ? jsonType( typeSchema ) : "object";
|
||||
entry["required"] = requiredFields.contains( fieldName );
|
||||
|
||||
const QString description = property.value( "description" ).toString();
|
||||
if ( !description.isEmpty() ) entry["description"] = description;
|
||||
|
||||
// Prefer the value already in input.yaml; fall back to the model default
|
||||
if ( configValues.contains( fieldName ) )
|
||||
entry["default"] = configValues.value( fieldName );
|
||||
else if ( property.contains( "default" ) )
|
||||
entry["default"] = property.value( "default" );
|
||||
|
||||
const QString format = typeSchema.value( "format" ).toString();
|
||||
if ( !format.isEmpty() ) entry["format"] = format;
|
||||
|
||||
if ( !resinsightType.isEmpty() ) entry["resinsight_type"] = resinsightType;
|
||||
|
||||
return entry;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
//==================================================================================================
|
||||
/// Helpers for the JSON schemas that taskmaestro generates for task input and output models
|
||||
//==================================================================================================
|
||||
namespace RimWorkflowSchemaTools
|
||||
{
|
||||
QJsonObject resolveRef( const QString& ref, const QJsonObject& rootSchema );
|
||||
QJsonObject resolveReferences( QJsonObject schema, const QJsonObject& rootSchema );
|
||||
QJsonObject underlyingSchema( QJsonObject schema, const QJsonObject& rootSchema );
|
||||
bool isOpaque( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QString objectPythonType( const QJsonObject& typeSchema, const QJsonObject& rootSchema );
|
||||
QString jsonType( const QJsonObject& typeSchema );
|
||||
bool isVec3Schema( const QJsonObject& typeSchema );
|
||||
QString resinsightTypeFromPythonType( const QString& pythonType );
|
||||
QStringList requiredFields( const QJsonObject& modelSchema );
|
||||
QStringList propertyNames( const QJsonObject& modelSchema );
|
||||
bool isConfigurable( const QJsonObject& property, const QJsonObject& rootSchema );
|
||||
|
||||
QJsonObject configFieldSchema( const QString& fieldName,
|
||||
const QJsonObject& inputSchema,
|
||||
const QStringList& requiredFields,
|
||||
const QJsonObject& configValues );
|
||||
} // namespace RimWorkflowSchemaTools
|
||||
@@ -45,7 +45,7 @@ void RimWorkflowStringBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowStringBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowStringBinding::toJsonValue() const
|
||||
{
|
||||
return hasValue() ? yamlQuotedScalar( m_value() ) : "null";
|
||||
return hasValue() ? QJsonValue( m_value() ) : QJsonValue( QJsonValue::Null );
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class RimWorkflowStringBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowStringBinding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowTaskCatalog.h"
|
||||
|
||||
#include "RimWorkflowHelperProcess.h"
|
||||
|
||||
#include "RiaLogging.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int catalogTimeoutMs = 120000;
|
||||
|
||||
std::optional<RimWorkflowTaskCatalog>& cachedCatalog()
|
||||
{
|
||||
static std::optional<RimWorkflowTaskCatalog> catalog;
|
||||
return catalog;
|
||||
}
|
||||
|
||||
RimWorkflowTaskCatalog loadCatalog()
|
||||
{
|
||||
auto result = RimWorkflowHelperProcess::runSync( { "catalog" }, {}, catalogTimeoutMs );
|
||||
if ( !result )
|
||||
{
|
||||
RiaLogging::warning( QString( "Could not load the workflow task catalog: %1" ).arg( result.error() ).toStdString() );
|
||||
return RimWorkflowTaskCatalog::failed( result.error() );
|
||||
}
|
||||
|
||||
auto catalog = RimWorkflowTaskCatalog::fromJson( result.value() );
|
||||
for ( const QString& error : catalog.pluginErrors() )
|
||||
RiaLogging::warning( QString( "Workflow plugin: %1" ).arg( error ).toStdString() );
|
||||
return catalog;
|
||||
}
|
||||
|
||||
QString pluginErrorText( const QJsonValue& error )
|
||||
{
|
||||
if ( error.isString() ) return error.toString();
|
||||
const QJsonObject object = error.toObject();
|
||||
const QString source = object.value( "id" ).toString( object.value( "entry_point" ).toString() );
|
||||
const QString text = object.value( "message" ).toString();
|
||||
return source.isEmpty() ? text : source + ": " + text;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowTaskCatalog RimWorkflowTaskCatalog::fromJson( const QJsonObject& catalog )
|
||||
{
|
||||
RimWorkflowTaskCatalog result;
|
||||
result.m_valid = true;
|
||||
result.m_taskmaestroVersion = catalog.value( "taskmaestro_version" ).toString();
|
||||
result.m_workflows = catalog.value( "workflows" ).toArray();
|
||||
for ( const QJsonValue& error : catalog.value( "errors" ).toArray() )
|
||||
result.m_pluginErrors.append( pluginErrorText( error ) );
|
||||
|
||||
for ( const QJsonValue& value : catalog.value( "tasks" ).toArray() )
|
||||
{
|
||||
const QJsonObject task = value.toObject();
|
||||
const QString id = task.value( "id" ).toString();
|
||||
if ( id.isEmpty() || result.m_taskTypes.contains( id ) ) continue;
|
||||
result.m_taskTypes.insert( id, task );
|
||||
result.m_taskOrder.append( id );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowTaskCatalog RimWorkflowTaskCatalog::failed( const QString& errorMessage )
|
||||
{
|
||||
RimWorkflowTaskCatalog result;
|
||||
result.m_loadError = errorMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
const RimWorkflowTaskCatalog& RimWorkflowTaskCatalog::instance()
|
||||
{
|
||||
auto& catalog = cachedCatalog();
|
||||
if ( !catalog ) catalog = loadCatalog();
|
||||
return *catalog;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskCatalog::invalidate()
|
||||
{
|
||||
cachedCatalog().reset();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskCatalog::setInstance( const RimWorkflowTaskCatalog& catalog )
|
||||
{
|
||||
cachedCatalog() = catalog;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowTaskCatalog::isValid() const
|
||||
{
|
||||
return m_valid;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskCatalog::loadError() const
|
||||
{
|
||||
return m_loadError;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QStringList RimWorkflowTaskCatalog::pluginErrors() const
|
||||
{
|
||||
return m_pluginErrors;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskCatalog::taskmaestroVersion() const
|
||||
{
|
||||
return m_taskmaestroVersion;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowTaskCatalog::taskType( const QString& taskId ) const
|
||||
{
|
||||
return m_taskTypes.value( taskId ).toObject();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Task descriptors in catalog order
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<QJsonObject> RimWorkflowTaskCatalog::tasks() const
|
||||
{
|
||||
std::vector<QJsonObject> result;
|
||||
result.reserve( m_taskOrder.size() );
|
||||
for ( const QString& id : m_taskOrder )
|
||||
result.push_back( m_taskTypes.value( id ).toObject() );
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonArray RimWorkflowTaskCatalog::registeredWorkflows() const
|
||||
{
|
||||
return m_workflows;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Add task types (from a workflow definition) that are not in the catalog
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskCatalog::merge( const QJsonObject& taskTypes )
|
||||
{
|
||||
for ( auto it = taskTypes.begin(); it != taskTypes.end(); ++it )
|
||||
{
|
||||
if ( m_taskTypes.contains( it.key() ) || !it.value().isObject() ) continue;
|
||||
m_taskTypes.insert( it.key(), it.value() );
|
||||
m_taskOrder.append( it.key() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <vector>
|
||||
|
||||
//==================================================================================================
|
||||
/// Task types and registered workflows published by installed packages (`taskmaestro_helper catalog`).
|
||||
/// The process-wide instance is loaded on first use and cached until `invalidate()` (Rescan).
|
||||
//==================================================================================================
|
||||
class RimWorkflowTaskCatalog
|
||||
{
|
||||
public:
|
||||
static RimWorkflowTaskCatalog fromJson( const QJsonObject& catalog );
|
||||
static RimWorkflowTaskCatalog failed( const QString& errorMessage );
|
||||
|
||||
static const RimWorkflowTaskCatalog& instance();
|
||||
static void invalidate();
|
||||
static void setInstance( const RimWorkflowTaskCatalog& catalog );
|
||||
|
||||
bool isValid() const;
|
||||
QString loadError() const;
|
||||
QStringList pluginErrors() const;
|
||||
QString taskmaestroVersion() const;
|
||||
|
||||
QJsonObject taskType( const QString& taskId ) const;
|
||||
std::vector<QJsonObject> tasks() const;
|
||||
QJsonArray registeredWorkflows() const;
|
||||
|
||||
void merge( const QJsonObject& taskTypes );
|
||||
|
||||
private:
|
||||
bool m_valid = false;
|
||||
QString m_loadError;
|
||||
QStringList m_pluginErrors;
|
||||
QString m_taskmaestroVersion;
|
||||
QJsonObject m_taskTypes;
|
||||
QStringList m_taskOrder;
|
||||
QJsonArray m_workflows;
|
||||
};
|
||||
@@ -15,7 +15,6 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowTaskInput.h"
|
||||
|
||||
#include "RimWorkflowArrayBinding.h"
|
||||
@@ -25,45 +24,20 @@
|
||||
#include "RimWorkflowFilePathBinding.h"
|
||||
#include "RimWorkflowFloatBinding.h"
|
||||
#include "RimWorkflowIntBinding.h"
|
||||
#include "RimWorkflowMappingBinding.h"
|
||||
#include "RimWorkflowStringBinding.h"
|
||||
#include "RimWorkflowVec3Binding.h"
|
||||
#include "RimWorkflowViewBinding.h"
|
||||
#include "RimWorkflowWellPathBinding.h"
|
||||
|
||||
#include "cafPdmUiOrdering.h"
|
||||
#include "cafPdmUiTextEditor.h"
|
||||
#include "cafPdmUiTreeOrdering.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowTaskInput, "WorkflowTaskInput" );
|
||||
|
||||
namespace
|
||||
{
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowFieldBinding* createBinding( const QJsonObject& schema )
|
||||
{
|
||||
const QString resinsightType = schema.value( "resinsight_type" ).toString();
|
||||
if ( resinsightType == "EclipseCase" ) return new RimWorkflowCaseBinding;
|
||||
if ( resinsightType == "WellPath" ) return new RimWorkflowWellPathBinding;
|
||||
if ( resinsightType == "View" ) return new RimWorkflowViewBinding;
|
||||
if ( resinsightType == "Vec3" ) return new RimWorkflowVec3Binding;
|
||||
|
||||
const QString type = schema.value( "type" ).toString( "string" );
|
||||
const QString format = schema.value( "format" ).toString();
|
||||
if ( type == "string" && format == "date" ) return new RimWorkflowDateBinding;
|
||||
if ( type == "string" && ( format == "path" || format == "file-path" || format == "directory-path" ) )
|
||||
return new RimWorkflowFilePathBinding;
|
||||
if ( type == "boolean" ) return new RimWorkflowBoolBinding;
|
||||
if ( type == "integer" ) return new RimWorkflowIntBinding;
|
||||
if ( type == "number" ) return new RimWorkflowFloatBinding;
|
||||
if ( type == "array" ) return new RimWorkflowArrayBinding;
|
||||
return new RimWorkflowStringBinding;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -74,6 +48,13 @@ RimWorkflowTaskInput::RimWorkflowTaskInput()
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_taskName, "TaskName", "Task" );
|
||||
m_taskName.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_taskType, "TaskType", "Task Type" );
|
||||
m_taskType.uiCapability()->setUiReadOnly( true );
|
||||
|
||||
CAF_PDM_InitFieldNoDefault( &m_taskDescription, "TaskDescription", "Description" );
|
||||
m_taskDescription.uiCapability()->setUiReadOnly( true );
|
||||
m_taskDescription.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -93,19 +74,168 @@ void RimWorkflowTaskInput::setTaskName( const QString& name )
|
||||
setUiName( name );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskInput::taskType() const
|
||||
{
|
||||
return m_taskType();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskInput::setTaskInfo( const QString& taskType, const QString& description )
|
||||
{
|
||||
m_taskType = taskType;
|
||||
m_taskDescription = description;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The class keyword of the binding used for a config field schema
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskInput::bindingClassKeyword( const QJsonObject& fieldSchema )
|
||||
{
|
||||
if ( fieldSchema.value( "map_over" ).toBool() ) return RimWorkflowMappingBinding::classKeywordStatic();
|
||||
|
||||
const QString resinsightType = fieldSchema.value( "resinsight_type" ).toString();
|
||||
if ( resinsightType == "EclipseCase" ) return RimWorkflowCaseBinding::classKeywordStatic();
|
||||
if ( resinsightType == "WellPath" ) return RimWorkflowWellPathBinding::classKeywordStatic();
|
||||
if ( resinsightType == "View" ) return RimWorkflowViewBinding::classKeywordStatic();
|
||||
if ( resinsightType == "Vec3" ) return RimWorkflowVec3Binding::classKeywordStatic();
|
||||
|
||||
const QString type = fieldSchema.value( "type" ).toString( "string" );
|
||||
const QString format = fieldSchema.value( "format" ).toString();
|
||||
if ( type == "string" && format == "date" ) return RimWorkflowDateBinding::classKeywordStatic();
|
||||
if ( type == "string" && ( format == "path" || format == "file-path" || format == "directory-path" ) )
|
||||
return RimWorkflowFilePathBinding::classKeywordStatic();
|
||||
if ( type == "boolean" ) return RimWorkflowBoolBinding::classKeywordStatic();
|
||||
if ( type == "integer" ) return RimWorkflowIntBinding::classKeywordStatic();
|
||||
if ( type == "number" ) return RimWorkflowFloatBinding::classKeywordStatic();
|
||||
if ( type == "array" ) return RimWorkflowArrayBinding::classKeywordStatic();
|
||||
return RimWorkflowStringBinding::classKeywordStatic();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowFieldBinding* RimWorkflowTaskInput::createBinding( const QJsonObject& fieldSchema )
|
||||
{
|
||||
const QString keyword = bindingClassKeyword( fieldSchema );
|
||||
if ( keyword == RimWorkflowCaseBinding::classKeywordStatic() ) return new RimWorkflowCaseBinding;
|
||||
if ( keyword == RimWorkflowWellPathBinding::classKeywordStatic() ) return new RimWorkflowWellPathBinding;
|
||||
if ( keyword == RimWorkflowViewBinding::classKeywordStatic() ) return new RimWorkflowViewBinding;
|
||||
if ( keyword == RimWorkflowVec3Binding::classKeywordStatic() ) return new RimWorkflowVec3Binding;
|
||||
if ( keyword == RimWorkflowDateBinding::classKeywordStatic() ) return new RimWorkflowDateBinding;
|
||||
if ( keyword == RimWorkflowFilePathBinding::classKeywordStatic() ) return new RimWorkflowFilePathBinding;
|
||||
if ( keyword == RimWorkflowBoolBinding::classKeywordStatic() ) return new RimWorkflowBoolBinding;
|
||||
if ( keyword == RimWorkflowIntBinding::classKeywordStatic() ) return new RimWorkflowIntBinding;
|
||||
if ( keyword == RimWorkflowFloatBinding::classKeywordStatic() ) return new RimWorkflowFloatBinding;
|
||||
if ( keyword == RimWorkflowArrayBinding::classKeywordStatic() ) return new RimWorkflowArrayBinding;
|
||||
if ( keyword == RimWorkflowMappingBinding::classKeywordStatic() ) return new RimWorkflowMappingBinding;
|
||||
return new RimWorkflowStringBinding;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskInput::detachedKey( const QString& fieldName, const QString& classKeyword )
|
||||
{
|
||||
return fieldName + "/" + classKeyword;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskInput::buildFromSchema( const QJsonArray& configFields )
|
||||
{
|
||||
QMap<QString, QJsonValue> noDetachedValues;
|
||||
deleteAllItems();
|
||||
for ( const QJsonValue& v : configFields )
|
||||
syncFromSchema( configFields, noDetachedValues );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Update the bindings to a new list of config fields. A binding is kept with its value when the
|
||||
/// field still has the same kind of binding. Values of removed bindings are moved to
|
||||
/// `detachedValues`, and restored from there when the field comes back.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskInput::syncFromSchema( const QJsonArray& configFields, QMap<QString, QJsonValue>& detachedValues )
|
||||
{
|
||||
std::vector<RimWorkflowFieldBinding*> existing = items();
|
||||
m_items.clearWithoutDelete();
|
||||
|
||||
for ( const QJsonValue& value : configFields )
|
||||
{
|
||||
const QJsonObject schema = v.toObject();
|
||||
auto* binding = createBinding( schema );
|
||||
QJsonObject schema = value.toObject();
|
||||
const QString field = schema.value( "name" ).toString();
|
||||
const QString keyword = bindingClassKeyword( schema );
|
||||
|
||||
RimWorkflowFieldBinding* binding = nullptr;
|
||||
for ( auto it = existing.begin(); it != existing.end(); ++it )
|
||||
{
|
||||
if ( *it && ( *it )->fieldName() == field && ( *it )->classKeyword() == keyword )
|
||||
{
|
||||
binding = *it;
|
||||
existing.erase( it );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( binding )
|
||||
{
|
||||
const QJsonValue current = binding->toJsonValue();
|
||||
// Keep a cleared value cleared instead of restoring the default
|
||||
if ( current.isNull() )
|
||||
schema.remove( "default" );
|
||||
else
|
||||
schema["default"] = current;
|
||||
}
|
||||
else
|
||||
{
|
||||
binding = createBinding( schema );
|
||||
const QString key = detachedKey( field, keyword );
|
||||
if ( detachedValues.contains( key ) ) schema["default"] = detachedValues.take( key );
|
||||
}
|
||||
binding->applySchema( schema );
|
||||
addItem( binding );
|
||||
}
|
||||
|
||||
for ( RimWorkflowFieldBinding* removed : existing )
|
||||
{
|
||||
if ( !removed ) continue;
|
||||
const QJsonValue value = removed->toJsonValue();
|
||||
if ( !value.isNull() ) detachedValues.insert( detachedKey( removed->fieldName(), removed->classKeyword() ), value );
|
||||
delete removed;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// All values that are set, including references to ResInsight objects
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowTaskInput::jsonValues() const
|
||||
{
|
||||
QJsonObject values;
|
||||
for ( const RimWorkflowFieldBinding* binding : items() )
|
||||
{
|
||||
const QJsonValue value = binding->toJsonValue();
|
||||
if ( !value.isNull() ) values.insert( binding->fieldName(), value );
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Values that can be stored in input.yaml, without references to objects in the project
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonObject RimWorkflowTaskInput::literalValues() const
|
||||
{
|
||||
QJsonObject values;
|
||||
for ( const RimWorkflowFieldBinding* binding : items() )
|
||||
{
|
||||
if ( binding->isObjectReference() ) continue;
|
||||
const QJsonValue value = binding->toJsonValue();
|
||||
if ( !value.isNull() ) values.insert( binding->fieldName(), value );
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -113,9 +243,12 @@ void RimWorkflowTaskInput::buildFromSchema( const QJsonArray& configFields )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowTaskInput::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
|
||||
{
|
||||
for ( RimWorkflowFieldBinding* b : items() )
|
||||
if ( !m_taskType().isEmpty() ) uiOrdering.add( &m_taskType );
|
||||
if ( !m_taskDescription().isEmpty() ) uiOrdering.add( &m_taskDescription );
|
||||
|
||||
for ( RimWorkflowFieldBinding* binding : items() )
|
||||
{
|
||||
if ( b && b->valueField() ) uiOrdering.add( b->valueField() );
|
||||
if ( binding && binding->valueField() ) uiOrdering.add( binding->valueField() );
|
||||
}
|
||||
uiOrdering.skipRemainingFields( true );
|
||||
}
|
||||
@@ -127,21 +260,3 @@ void RimWorkflowTaskInput::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeO
|
||||
{
|
||||
uiTreeOrdering.skipRemainingChildren( true );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowTaskInput::toTaskYamlBlock() const
|
||||
{
|
||||
QString out = m_taskName() + ":\n";
|
||||
if ( count() == 0 )
|
||||
{
|
||||
out += " {}\n";
|
||||
return out;
|
||||
}
|
||||
for ( const RimWorkflowFieldBinding* b : items() )
|
||||
{
|
||||
out += QString( " %1: %2\n" ).arg( b->fieldName(), b->toYamlValue() );
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -15,15 +15,22 @@
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowFieldBinding.h"
|
||||
|
||||
#include "cafPdmObjectCollection.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QMap>
|
||||
|
||||
class QJsonArray;
|
||||
|
||||
//==================================================================================================
|
||||
/// The input values of one task instance in a job. There is one per task in the workflow; tasks
|
||||
/// without config fields have no bindings and are hidden.
|
||||
//==================================================================================================
|
||||
class RimWorkflowTaskInput : public caf::PdmObjectCollection<RimWorkflowFieldBinding>
|
||||
{
|
||||
CAF_PDM_HEADER_INIT;
|
||||
@@ -33,10 +40,18 @@ public:
|
||||
|
||||
QString taskName() const;
|
||||
void setTaskName( const QString& name );
|
||||
QString taskType() const;
|
||||
void setTaskInfo( const QString& taskType, const QString& description );
|
||||
|
||||
void buildFromSchema( const QJsonArray& configFields );
|
||||
void syncFromSchema( const QJsonArray& configFields, QMap<QString, QJsonValue>& detachedValues );
|
||||
|
||||
QString toTaskYamlBlock() const;
|
||||
QJsonObject jsonValues() const;
|
||||
QJsonObject literalValues() const;
|
||||
|
||||
static QString bindingClassKeyword( const QJsonObject& fieldSchema );
|
||||
static RimWorkflowFieldBinding* createBinding( const QJsonObject& fieldSchema );
|
||||
static QString detachedKey( const QString& fieldName, const QString& classKeyword );
|
||||
|
||||
protected:
|
||||
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
|
||||
@@ -44,4 +59,6 @@ protected:
|
||||
|
||||
private:
|
||||
caf::PdmField<QString> m_taskName;
|
||||
caf::PdmField<QString> m_taskType;
|
||||
caf::PdmField<QString> m_taskDescription;
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RimWorkflowValidationTools.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
namespace
|
||||
{
|
||||
RimWorkflowIssue errorIssue( const QString& task, const QString& field, const QString& message )
|
||||
{
|
||||
return { .task = task, .field = field, .severity = RimWorkflowIssue::Severity::Error, .message = message };
|
||||
}
|
||||
|
||||
void appendErrorIssues( const QJsonObject& error, const QStringList& nodeNames, std::vector<RimWorkflowIssue>& issues )
|
||||
{
|
||||
QString message = error.value( "message" ).toString();
|
||||
if ( message.isEmpty() ) message = "The workflow is not valid";
|
||||
|
||||
QString task = error.value( "task" ).toString();
|
||||
if ( !nodeNames.contains( task ) ) task = RimWorkflowValidationTools::taskFromMessage( message, nodeNames );
|
||||
|
||||
const QJsonArray fieldIssues = error.value( "issues" ).toArray();
|
||||
if ( fieldIssues.isEmpty() || task.isEmpty() )
|
||||
{
|
||||
issues.push_back( errorIssue( task, error.value( "field" ).toString(), message ) );
|
||||
return;
|
||||
}
|
||||
|
||||
for ( const QJsonValue& value : fieldIssues )
|
||||
{
|
||||
const QJsonObject fieldIssue = value.toObject();
|
||||
const QString code = fieldIssue.value( "code" ).toString();
|
||||
issues.push_back(
|
||||
errorIssue( task, fieldIssue.value( "field" ).toString(), code.isEmpty() ? message : QString( "%1 (%2)" ).arg( message, code ) ) );
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowValidationTools::Result RimWorkflowValidationTools::issuesFromHelperResult( const QJsonObject& helperResult,
|
||||
const QStringList& nodeNames )
|
||||
{
|
||||
if ( helperResult.value( "status" ).toString() == "invalid" )
|
||||
{
|
||||
Result result;
|
||||
result.describeSucceeded = false;
|
||||
appendErrorIssues( helperResult.value( "error" ).toObject(), nodeNames, result.issues );
|
||||
return result;
|
||||
}
|
||||
return issuesFromDescribe( helperResult.value( "describe" ).toObject(), helperResult.value( "describe_error" ).toObject(), nodeNames );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Errors from describe, and warnings for config fields without a value
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
RimWorkflowValidationTools::Result RimWorkflowValidationTools::issuesFromDescribe( const QJsonObject& describe,
|
||||
const QJsonObject& describeError,
|
||||
const QStringList& nodeNames )
|
||||
{
|
||||
Result result;
|
||||
if ( !describeError.isEmpty() )
|
||||
{
|
||||
result.describeSucceeded = false;
|
||||
appendErrorIssues( describeError, nodeNames, result.issues );
|
||||
return result;
|
||||
}
|
||||
|
||||
for ( const QJsonValue& value : describe.value( "tasks" ).toArray() )
|
||||
{
|
||||
const QJsonObject task = value.toObject();
|
||||
for ( const QJsonValue& field : task.value( "missing_config_fields" ).toArray() )
|
||||
{
|
||||
result.issues.push_back( { .task = task.value( "name" ).toString(),
|
||||
.field = field.toString(),
|
||||
.severity = RimWorkflowIssue::Severity::Warning,
|
||||
.message = missingValueMessage( field.toString() ) } );
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The node whose name is quoted in the message, preferring the longest name
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowValidationTools::taskFromMessage( const QString& message, const QStringList& nodeNames )
|
||||
{
|
||||
QString found;
|
||||
for ( const QString& name : nodeNames )
|
||||
{
|
||||
const bool quoted = message.contains( QString( "'%1'" ).arg( name ) ) || message.contains( QString( "\"%1\"" ).arg( name ) );
|
||||
if ( quoted && name.size() > found.size() ) found = name;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowValidationTools::missingValueMessage( const QString& fieldName )
|
||||
{
|
||||
return QString( "Input '%1' has no value" ).arg( fieldName );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The saved definition only carries literal values, so describe reports object references (cases,
|
||||
/// well paths, views) as missing, and the result goes stale when the user edits a value. The job's
|
||||
/// own bindings are the source of truth for the fields they cover.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QJsonArray RimWorkflowValidationTools::issuesWithJobValues( const QString& taskName,
|
||||
const QJsonArray& taskIssues,
|
||||
const std::map<QString, bool>& fieldHasValue )
|
||||
{
|
||||
QJsonArray result;
|
||||
for ( const QJsonValue& value : taskIssues )
|
||||
{
|
||||
const QJsonObject issue = value.toObject();
|
||||
const QString field = issue.value( "field" ).toString();
|
||||
const bool isMissingValueWarning = issue.value( "severity" ).toString() == "warning" &&
|
||||
issue.value( "message" ).toString() == missingValueMessage( field );
|
||||
if ( isMissingValueWarning && fieldHasValue.contains( field ) ) continue;
|
||||
result.append( issue );
|
||||
}
|
||||
|
||||
for ( const auto& [field, hasValue] : fieldHasValue )
|
||||
{
|
||||
if ( hasValue ) continue;
|
||||
result.append(
|
||||
QJsonObject{ { "task", taskName }, { "field", field }, { "severity", "warning" }, { "message", missingValueMessage( field ) } } );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2026- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight 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 General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RimWorkflowDefinition.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
//==================================================================================================
|
||||
/// Turns the result of `taskmaestro_helper save --describe` (or `load --describe`) into issues
|
||||
/// attached to the tasks of the workflow. Issues with an empty task belong to the workflow.
|
||||
//==================================================================================================
|
||||
namespace RimWorkflowValidationTools
|
||||
{
|
||||
struct Result
|
||||
{
|
||||
std::vector<RimWorkflowIssue> issues;
|
||||
bool describeSucceeded = true;
|
||||
};
|
||||
|
||||
Result issuesFromHelperResult( const QJsonObject& helperResult, const QStringList& nodeNames );
|
||||
Result issuesFromDescribe( const QJsonObject& describe, const QJsonObject& describeError, const QStringList& nodeNames );
|
||||
QString taskFromMessage( const QString& message, const QStringList& nodeNames );
|
||||
QString missingValueMessage( const QString& fieldName );
|
||||
|
||||
// Replace the missing-value warnings of a task with warnings derived from the values of a job.
|
||||
// `fieldHasValue` maps the task's bound input fields to whether the job gives them a value.
|
||||
QJsonArray issuesWithJobValues( const QString& taskName, const QJsonArray& taskIssues, const std::map<QString, bool>& fieldHasValue );
|
||||
} // namespace RimWorkflowValidationTools
|
||||
@@ -50,13 +50,10 @@ void RimWorkflowVec3Binding::applySchema( const QJsonObject& fieldSchema )
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowVec3Binding::toYamlValue() const
|
||||
QJsonValue RimWorkflowVec3Binding::toJsonValue() const
|
||||
{
|
||||
if ( !hasValue() ) return "null";
|
||||
if ( !hasValue() ) return QJsonValue::Null;
|
||||
|
||||
const cvf::Vec3d& v = m_value();
|
||||
return QString( "{x: %1, y: %2, z: %3}" )
|
||||
.arg( QString::number( v.x(), 'g', 17 ) )
|
||||
.arg( QString::number( v.y(), 'g', 17 ) )
|
||||
.arg( QString::number( v.z(), 'g', 17 ) );
|
||||
return QJsonObject{ { "x", v.x() }, { "y", v.y() }, { "z", v.z() } };
|
||||
}
|
||||
@@ -31,8 +31,8 @@ class RimWorkflowVec3Binding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowVec3Binding();
|
||||
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString toYamlValue() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_value; }
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "RimEclipseView.h"
|
||||
#include "RimProject.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowViewBinding, "WorkflowViewBinding" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -44,10 +46,10 @@ QString RimWorkflowViewBinding::displayValue() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowViewBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowViewBinding::toJsonValue() const
|
||||
{
|
||||
if ( m_view() == nullptr ) return "null";
|
||||
return QString( "{__resinsight_ref__: View, view_id: %1}" ).arg( m_view()->id() );
|
||||
if ( m_view() == nullptr ) return QJsonValue::Null;
|
||||
return QJsonObject{ { "__resinsight_ref__", "View" }, { "view_id", m_view()->id() } };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -73,3 +75,31 @@ QList<caf::PdmOptionItemInfo> RimWorkflowViewBinding::calculateValueOptions( con
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowViewBinding::isObjectReference() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowViewBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
{
|
||||
RimWorkflowFieldBinding::applySchema( fieldSchema );
|
||||
const QJsonObject reference = fieldSchema.value( "default" ).toObject();
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() != "View" || !reference.contains( "view_id" ) ) return;
|
||||
|
||||
const int viewId = reference.value( "view_id" ).toInt( -1 );
|
||||
for ( RimEclipseCase* eclipseCase : RimEclipseCaseTools::eclipseCases() )
|
||||
{
|
||||
if ( !eclipseCase ) continue;
|
||||
for ( RimEclipseView* view : eclipseCase->reservoirViews() )
|
||||
{
|
||||
if ( view && view->id() == viewId ) m_view = view;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,10 @@ class RimWorkflowViewBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowViewBinding();
|
||||
|
||||
QString toYamlValue() const override;
|
||||
QString displayValue() const override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
bool isObjectReference() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString displayValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_view; }
|
||||
|
||||
|
||||
@@ -18,9 +18,12 @@
|
||||
|
||||
#include "RimWorkflowWellPathBinding.h"
|
||||
|
||||
#include "RimProject.h"
|
||||
#include "RimTools.h"
|
||||
#include "RimWellPath.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWorkflowWellPathBinding, "WorkflowWellPathBinding" );
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -42,11 +45,10 @@ QString RimWorkflowWellPathBinding::displayValue() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
QString RimWorkflowWellPathBinding::toYamlValue() const
|
||||
QJsonValue RimWorkflowWellPathBinding::toJsonValue() const
|
||||
{
|
||||
if ( m_wellPath() == nullptr ) return "null";
|
||||
const QString name = yamlQuotedScalar( m_wellPath()->name() );
|
||||
return QString( "{__resinsight_ref__: WellPath, well_path_name: %1}" ).arg( name );
|
||||
if ( m_wellPath() == nullptr ) return QJsonValue::Null;
|
||||
return QJsonObject{ { "__resinsight_ref__", "WellPath" }, { "well_path_name", m_wellPath()->name() } };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -58,3 +60,29 @@ QList<caf::PdmOptionItemInfo> RimWorkflowWellPathBinding::calculateValueOptions(
|
||||
if ( fieldNeedingOptions == &m_wellPath ) RimTools::wellPathOptionItems( &options );
|
||||
return options;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
bool RimWorkflowWellPathBinding::isObjectReference() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWorkflowWellPathBinding::applySchema( const QJsonObject& fieldSchema )
|
||||
{
|
||||
RimWorkflowFieldBinding::applySchema( fieldSchema );
|
||||
const QJsonObject reference = fieldSchema.value( "default" ).toObject();
|
||||
if ( reference.value( "__resinsight_ref__" ).toString() != "WellPath" || !reference.contains( "well_path_name" ) ) return;
|
||||
|
||||
auto* project = RimProject::current();
|
||||
if ( !project ) return;
|
||||
const QString name = reference.value( "well_path_name" ).toString();
|
||||
for ( RimWellPath* wellPath : project->allWellPaths() )
|
||||
{
|
||||
if ( wellPath && wellPath->name() == name ) m_wellPath = wellPath;
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,10 @@ class RimWorkflowWellPathBinding : public RimWorkflowFieldBinding
|
||||
public:
|
||||
RimWorkflowWellPathBinding();
|
||||
|
||||
QString toYamlValue() const override;
|
||||
QString displayValue() const override;
|
||||
QJsonValue toJsonValue() const override;
|
||||
bool isObjectReference() const override;
|
||||
void applySchema( const QJsonObject& fieldSchema ) override;
|
||||
QString displayValue() const override;
|
||||
|
||||
caf::PdmFieldHandle* valueField() override { return &m_wellPath; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user