mirror of
https://github.com/OPM/ResInsight.git
synced 2026-11-13 09:37:29 -06:00
Collapse a task with the chevron in its title, a double-click on the title, or the context menus. A collapsed task shows its title, state and issue count, and its connections meet in one dot on each side. Ports and config values are shown in the tooltip. The collapsed set is stored per workflow.
975 lines
41 KiB
C++
975 lines
41 KiB
C++
/////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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 "RimWorkflow.h"
|
|
|
|
#include "RimWorkflowDefinitionTools.h"
|
|
#include "RimWorkflowDescribeTools.h"
|
|
#include "RimWorkflowHelperProcess.h"
|
|
#include "RimWorkflowJob.h"
|
|
#include "RimWorkflowValidationTools.h"
|
|
|
|
#include "RiaLogging.h"
|
|
#include "RiaPreferences.h"
|
|
|
|
#include "RiuMainWindow.h"
|
|
|
|
#include "cafCmdFeatureMenuBuilder.h"
|
|
#include "cafPdmUiOrdering.h"
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QStandardPaths>
|
|
#include <QTemporaryDir>
|
|
|
|
#include <algorithm>
|
|
|
|
CAF_PDM_SOURCE_INIT( RimWorkflow, "Workflow" );
|
|
|
|
namespace
|
|
{
|
|
constexpr size_t maxUndoSteps = 100;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow::RimWorkflow()
|
|
{
|
|
CAF_PDM_InitObject( "Workflow", ":/Folder.png" );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_name, "Name", "Name" );
|
|
m_name.uiCapability()->setUiReadOnly( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_description, "Description", "Description" );
|
|
m_description.uiCapability()->setUiReadOnly( true );
|
|
|
|
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", "" );
|
|
|
|
CAF_PDM_InitField( &m_inlineConfigValues, "InlineConfigValues", true, "Inline Config Values" );
|
|
m_inlineConfigValues.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_hiddenTasks, "HiddenTasks", "Hidden Tasks" );
|
|
m_hiddenTasks.uiCapability()->setUiHidden( true );
|
|
|
|
CAF_PDM_InitFieldNoDefault( &m_collapsedTasks, "CollapsedTasks", "Collapsed Tasks" );
|
|
m_collapsedTasks.uiCapability()->setUiHidden( true );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
RimWorkflow::~RimWorkflow() = default;
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::name() const
|
|
{
|
|
return m_name();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setWorkflowDirectory( const QString& directory )
|
|
{
|
|
m_workflowDirectory = directory;
|
|
m_sourceDirectory = directory;
|
|
m_name = QFileInfo( QDir( directory ).absolutePath() ).fileName();
|
|
updateUiName();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::workflowDirectory() const
|
|
{
|
|
return m_workflowDirectory().path();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QJsonObject RimWorkflow::graph() const
|
|
{
|
|
return m_graph;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::loadError() const
|
|
{
|
|
return m_loadError();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Whether the graph shows config values in the task boxes instead of in separate config boxes
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::inlineConfigValues() const
|
|
{
|
|
return m_inlineConfigValues();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setInlineConfigValues( bool inlineValues )
|
|
{
|
|
m_inlineConfigValues = inlineValues;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Tasks the user has hidden in the graph. A view setting: the definition is not changed.
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::hiddenTasks() const
|
|
{
|
|
return QStringList( m_hiddenTasks().begin(), m_hiddenTasks().end() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isTaskHidden( const QString& taskName ) const
|
|
{
|
|
return std::ranges::contains( m_hiddenTasks(), taskName );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setTaskHidden( const QString& taskName, bool hidden )
|
|
{
|
|
std::vector<QString> tasks = m_hiddenTasks();
|
|
std::erase( tasks, taskName );
|
|
if ( hidden ) tasks.push_back( taskName );
|
|
m_hiddenTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::showAllTasks()
|
|
{
|
|
m_hiddenTasks = std::vector<QString>();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// Tasks the user has collapsed to their title in the graph. A view setting like the hidden tasks.
|
|
//--------------------------------------------------------------------------------------------------
|
|
QStringList RimWorkflow::collapsedTasks() const
|
|
{
|
|
return QStringList( m_collapsedTasks().begin(), m_collapsedTasks().end() );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
bool RimWorkflow::isTaskCollapsed( const QString& taskName ) const
|
|
{
|
|
return std::ranges::contains( m_collapsedTasks(), taskName );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setTaskCollapsed( const QString& taskName, bool collapsed )
|
|
{
|
|
std::vector<QString> tasks = m_collapsedTasks();
|
|
std::erase( tasks, taskName );
|
|
if ( collapsed ) tasks.push_back( taskName );
|
|
m_collapsedTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setCollapsedTasks( const QStringList& taskNames )
|
|
{
|
|
std::vector<QString> tasks;
|
|
for ( const QString& taskName : taskNames )
|
|
{
|
|
if ( !std::ranges::contains( tasks, taskName ) ) tasks.push_back( taskName );
|
|
}
|
|
m_collapsedTasks = tasks;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
std::vector<RimWorkflowJob*> RimWorkflow::jobs() const
|
|
{
|
|
std::vector<RimWorkflowJob*> result;
|
|
result.reserve( m_jobs.size() );
|
|
for ( RimWorkflowJob* job : m_jobs.childrenByType() )
|
|
{
|
|
if ( job ) result.push_back( job );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::addJob( RimWorkflowJob* 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 );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
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_graph = {};
|
|
m_loadError = "";
|
|
|
|
const QString directory = workflowDirectory();
|
|
if ( directory.isEmpty() ) return false;
|
|
if ( !QDir( directory ).exists( "workflow.yaml" ) ) return setLoadError( "Missing workflow.yaml", errorMessage );
|
|
|
|
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 = RimWorkflowHelperProcess::errorMessage( m_definition.describeError );
|
|
RiaLogging::warning( QString( "Workflow '%1' is not valid: %2" ).arg( m_name(), m_loadError() ).toStdString() );
|
|
}
|
|
|
|
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() )
|
|
{
|
|
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;
|
|
}
|
|
|
|
// `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() )
|
|
{
|
|
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();
|
|
|
|
setDefinition( definition, Source::Registered );
|
|
if ( m_graph.value( "tasks" ).toArray().isEmpty() ) return setLoadError( "The workflow could not be described", errorMessage );
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
/// 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;
|
|
|
|
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 );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
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;
|
|
|
|
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() )
|
|
{
|
|
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;
|
|
|
|
// Hidden and collapsed tasks follow renames, and deleted tasks are forgotten
|
|
for ( auto* field : { &m_hiddenTasks, &m_collapsedTasks } )
|
|
{
|
|
std::vector<QString> tasks;
|
|
for ( const QString& taskName : field->v() )
|
|
{
|
|
const QString name = renames.value( taskName, taskName );
|
|
if ( m_definition.findNode( name ) && !std::ranges::contains( tasks, name ) ) tasks.push_back( name );
|
|
}
|
|
*field = tasks;
|
|
}
|
|
|
|
// 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" );
|
|
m_jobs.push_back( job );
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
void RimWorkflow::setValidationResult( int revision, const std::vector<RimWorkflowIssue>& issues, bool describeSucceeded )
|
|
{
|
|
if ( revision != m_revision ) return;
|
|
|
|
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 {};
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
///
|
|
//--------------------------------------------------------------------------------------------------
|
|
QString RimWorkflow::findPythonExecutable()
|
|
{
|
|
QStringList candidates;
|
|
if ( auto* prefs = RiaPreferences::current() )
|
|
{
|
|
QString configured = prefs->pythonExecutable();
|
|
if ( !configured.isEmpty() && configured != "python" ) candidates << configured;
|
|
}
|
|
candidates << "python3" << "python";
|
|
|
|
for ( const QString& candidate : candidates )
|
|
{
|
|
if ( candidate.contains( '/' ) || candidate.contains( '\\' ) )
|
|
{
|
|
if ( QFileInfo( candidate ).isExecutable() ) return candidate;
|
|
}
|
|
else if ( !QStandardPaths::findExecutable( candidate ).isEmpty() )
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
return {};
|
|
}
|