mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 22:27:55 -05:00
Workflows: Show port types in the graph view
Each input and output port shows its type below the name, for editable and read-only workflows. The graph JSON carries input_types/output_types per task, built from the task schemas.
This commit is contained in:
1 parent
6e2fbde65f
commit
ec388ba34b
8 files changed
+87
-24
No files matched your search
@@ -286,6 +286,8 @@ QJsonObject graphTask( const RimWorkflowDefinition& definition,
|
||||
{ "whole_input", inputPorts.size() == 1 && !inputSchema.value( "properties" ).toObject().isEmpty() },
|
||||
{ "input_ports", inputPortsJson( definition, node, inputPorts, definition.incomingEdges( node.name ) ) },
|
||||
{ "output_ports", outputPortsJson( outputPorts, definition.outgoingEdges( node.name ) ) },
|
||||
{ "input_types", RimWorkflowPortCompatibility::portTypes( inputPorts ) },
|
||||
{ "output_types", RimWorkflowPortCompatibility::portTypes( outputPorts ) },
|
||||
{ "is_result", node.name == resultTask },
|
||||
{ "issues", taskIssues } };
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "RimWorkflowDescribeTools.h"
|
||||
|
||||
#include "RimWorkflowPortCompatibility.h"
|
||||
#include "RimWorkflowSchemaTools.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
@@ -117,7 +118,9 @@ QJsonObject taskFromDescribe( const QJsonObject& task )
|
||||
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 ) ) } };
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -346,6 +346,17 @@ QString RimWorkflowPortCompatibility::typeName( const QJsonObject& schema, const
|
||||
return pythonNames.value( type, type );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// 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
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -55,9 +55,10 @@ std::optional<RimWorkflowPort> inputPort( const QJsonObject& taskType, const QSt
|
||||
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 );
|
||||
bool isCompatible( const RimWorkflowPort& produced, const RimWorkflowPort& expected );
|
||||
bool isAny( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QString typeName( const QJsonObject& schema, const QJsonObject& rootSchema );
|
||||
QJsonObject portTypes( const std::vector<RimWorkflowPort>& ports );
|
||||
|
||||
QStringList coveredByWholeOutput( const QJsonObject& upstreamTaskType, const QJsonObject& downstreamTaskType );
|
||||
bool wouldCreateCycle( const RimWorkflowDefinition& definition, const QString& from, const QString& to );
|
||||
|
||||
@@ -159,6 +159,10 @@ TEST( RimWorkflowDefinition, graphFromDefinition )
|
||||
EXPECT_EQ( "resinsight", ports[1].toObject().value( "name" ).toString() );
|
||||
EXPECT_TRUE( ports[1].toObject().value( "wired" ).toBool() );
|
||||
|
||||
// Type names for labelling the ports
|
||||
EXPECT_EQ( "RipsInstance", exportTask.value( "input_types" ).toObject().value( "resinsight" ).toString() );
|
||||
EXPECT_EQ( "str", exportTask.value( "output_types" ).toObject().value( "export_file" ).toString() );
|
||||
|
||||
// The connect task's opaque `value` is not a port of its own
|
||||
const QJsonObject connect = taskByName( graph, "connect_to_resinsight" );
|
||||
EXPECT_TRUE( connect.value( "outputs" ).toArray().isEmpty() );
|
||||
|
||||
@@ -82,6 +82,8 @@ TEST( RimWorkflowDescribeTools, configFieldTypesAndValues )
|
||||
EXPECT_EQ( "Person to greet", name.value( "description" ).toString() );
|
||||
EXPECT_FALSE( name.value( "required" ).toBool() );
|
||||
EXPECT_EQ( "world", fieldByName( greetB, "name" ).value( "default" ).toString() );
|
||||
EXPECT_EQ( "str", greetA.value( "input_types" ).toObject().value( "name" ).toString() );
|
||||
EXPECT_EQ( "int", greetA.value( "input_types" ).toObject().value( "times" ).toString() );
|
||||
|
||||
const QJsonObject times = fieldByName( greetA, "times" );
|
||||
EXPECT_EQ( "integer", times.value( "type" ).toString() );
|
||||
|
||||
@@ -122,6 +122,7 @@ struct PortInfo
|
||||
QString key;
|
||||
QString label;
|
||||
QString toolTip;
|
||||
QString typeName{};
|
||||
Style style = Style::Normal;
|
||||
};
|
||||
|
||||
@@ -369,15 +370,32 @@ void GraphNode::addPorts( const std::vector<PortInfo>& ports, bool output )
|
||||
auto* port = new PortItem( this, info, output, m_isConfig, QPointF( x, y ) );
|
||||
portMap.insert( info.key, port );
|
||||
|
||||
QFont font;
|
||||
font.setPointSize( 9 );
|
||||
const QString display = QFontMetrics( font ).elidedText( info.label, Qt::ElideRight, m_isConfig ? m_width - 24 : m_width / 2 - 18 );
|
||||
auto* text = new QGraphicsTextItem( display, this );
|
||||
text->setFont( font );
|
||||
text->setToolTip( info.toolTip.isEmpty() ? info.label : info.toolTip );
|
||||
text->setDefaultTextColor( info.style == PortInfo::Style::Missing ? missingPortColor : QColor( 45, 65, 80 ) );
|
||||
text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10, y - ( m_isConfig || !output ? 20 : 13 ) );
|
||||
text->setAcceptedMouseButtons( Qt::NoButton );
|
||||
const QString toolTip = info.toolTip.isEmpty() ? info.label : info.toolTip;
|
||||
const qreal textWidth = m_isConfig ? m_width - 24 : m_width / 2 - 18;
|
||||
const bool showType = !info.typeName.isEmpty();
|
||||
auto addText = [&]( const QString& content, const QFont& font, const QColor& color, qreal top )
|
||||
{
|
||||
auto* text = new QGraphicsTextItem( QFontMetrics( font ).elidedText( content, Qt::ElideRight, textWidth ), this );
|
||||
text->setFont( font );
|
||||
text->setToolTip( toolTip );
|
||||
text->setDefaultTextColor( color );
|
||||
text->setPos( output ? m_width - text->boundingRect().width() - 12 : 10, top );
|
||||
text->setAcceptedMouseButtons( Qt::NoButton );
|
||||
};
|
||||
|
||||
QFont labelFont;
|
||||
labelFont.setPointSize( 9 );
|
||||
const QColor labelColor = info.style == PortInfo::Style::Missing ? missingPortColor : QColor( 45, 65, 80 );
|
||||
addText( info.label, labelFont, labelColor, y - ( m_isConfig || !output || showType ? 20 : 13 ) );
|
||||
|
||||
// The type goes on a second, smaller line below the label
|
||||
if ( showType )
|
||||
{
|
||||
QFont typeFont;
|
||||
typeFont.setPointSize( 7 );
|
||||
typeFont.setItalic( true );
|
||||
addText( info.typeName, typeFont, QColor( 110, 120, 135 ), y - 3 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,8 +699,9 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
|
||||
if ( field.isEmpty() ) hasWholeInput = true;
|
||||
|
||||
PortInfo info;
|
||||
info.key = keyForField( field );
|
||||
info.label = field.isEmpty() ? "input" : field;
|
||||
info.key = keyForField( field );
|
||||
info.label = field.isEmpty() ? "input" : field;
|
||||
info.typeName = port.value( "type" ).toString();
|
||||
QStringList toolTip{ QString( "%1: %2" ).arg( info.label, port.value( "type" ).toString( "any" ) ) };
|
||||
if ( !port.value( "description" ).toString().isEmpty() ) toolTip << port.value( "description" ).toString();
|
||||
toolTip << ( required ? "Required" : "Optional" );
|
||||
@@ -697,7 +716,11 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
|
||||
const QJsonObject edge = value.toObject();
|
||||
if ( edge.value( "to" ).toString() == name && edge.value( "input" ).toString().isEmpty() && !hasWholeInput )
|
||||
{
|
||||
inputs.insert( inputs.begin(), PortInfo{ .key = "model", .label = "input", .toolTip = {} } );
|
||||
inputs.insert( inputs.begin(),
|
||||
PortInfo{ .key = "model",
|
||||
.label = "input",
|
||||
.toolTip = {},
|
||||
.typeName = task.value( "input_types" ).toObject().value( "" ).toString() } );
|
||||
hasWholeInput = true;
|
||||
}
|
||||
}
|
||||
@@ -708,25 +731,37 @@ std::pair<std::vector<PortInfo>, std::vector<PortInfo>> editModePorts( const QJs
|
||||
const QJsonObject port = value.toObject();
|
||||
const QString field = port.value( "name" ).toString();
|
||||
PortInfo info;
|
||||
info.key = keyForField( field );
|
||||
info.label = field.isEmpty() ? "output" : field;
|
||||
info.key = keyForField( field );
|
||||
info.label = field.isEmpty() ? "output" : field;
|
||||
info.typeName = port.value( "type" ).toString();
|
||||
QStringList toolTip{ QString( "%1: %2" ).arg( info.label, port.value( "type" ).toString( "any" ) ) };
|
||||
if ( !port.value( "description" ).toString().isEmpty() ) toolTip << port.value( "description" ).toString();
|
||||
info.toolTip = toolTip.join( "\n" );
|
||||
outputs.push_back( info );
|
||||
}
|
||||
if ( outputs.empty() ) outputs.push_back( PortInfo{ .key = "model", .label = "output", .toolTip = {} } );
|
||||
if ( outputs.empty() )
|
||||
outputs.push_back( PortInfo{ .key = "model",
|
||||
.label = "output",
|
||||
.toolTip = {},
|
||||
.typeName = task.value( "output_types" ).toObject().value( "" ).toString() } );
|
||||
return { inputs, outputs };
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<PortInfo> portInfos( const QStringList& keys, bool output )
|
||||
std::vector<PortInfo> portInfos( const QStringList& keys, bool output, const QJsonObject& types )
|
||||
{
|
||||
std::vector<PortInfo> result;
|
||||
for ( const QString& key : keys )
|
||||
result.push_back( PortInfo{ .key = key, .label = key == "model" ? ( output ? "output" : "input" ) : key.mid( 6 ), .toolTip = {} } );
|
||||
{
|
||||
const QString typeName = types.value( fieldForKey( key ) ).toString();
|
||||
const QString label = key == "model" ? ( output ? "output" : "input" ) : fieldForKey( key );
|
||||
result.push_back( PortInfo{ .key = key,
|
||||
.label = label,
|
||||
.toolTip = typeName.isEmpty() ? label : QString( "%1: %2" ).arg( label, typeName ),
|
||||
.typeName = typeName } );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
@@ -922,8 +957,8 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e
|
||||
out.removeDuplicates();
|
||||
in.sort();
|
||||
out.sort();
|
||||
inputs = portInfos( in, false );
|
||||
outputs = portInfos( out, true );
|
||||
inputs = portInfos( in, false, data.value( "input_types" ).toObject() );
|
||||
outputs = portInfos( out, true, data.value( "output_types" ).toObject() );
|
||||
}
|
||||
|
||||
auto* node = new GraphNode( name, inputs, outputs );
|
||||
@@ -943,7 +978,8 @@ void RiuWorkflowGraphView::showGraph( const QJsonObject& graph, const QString& e
|
||||
|
||||
if ( !configFields.value( name ).isEmpty() )
|
||||
{
|
||||
auto* configNode = new GraphNode( name, {}, portInfos( configFields.value( name ), true ), true );
|
||||
auto* configNode =
|
||||
new GraphNode( name, {}, portInfos( configFields.value( name ), true, data.value( "input_types" ).toObject() ), true );
|
||||
m_scene->addItem( configNode );
|
||||
m_configNodes.insert( name, configNode );
|
||||
}
|
||||
|
||||
@@ -154,6 +154,10 @@ in a central dock tab:
|
||||
- a status line for load errors, read-only reasons, validation results and
|
||||
unsaved changes.
|
||||
|
||||
Each port shows its name with its type below it in smaller text (from the
|
||||
task schemas, as `input_types`/`output_types` in the graph JSON). The
|
||||
tooltip also shows the description.
|
||||
|
||||
`RiuWorkflowGraphLayout` computes the layout: topological ranks with a Kahn
|
||||
sort and longest-path ranks, then one barycenter pass to order nodes. It
|
||||
tolerates cycles. The layout is always automatic; nodes moved by hand keep
|
||||
|
||||
Reference in new issue
Block a user