Skip to content

POC_workflow_builder #834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/dashflow_black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/dashflow_blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions lib/providers/ui_providers.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:apidash/consts.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/screens/dashflow/dashflow_builder/nodes.dart';

final mobileScaffoldKeyStateProvider =
StateProvider<GlobalKey<ScaffoldState>>((ref) => kHomeScaffoldKey);
Expand Down Expand Up @@ -37,3 +38,47 @@ final environmentSearchQueryProvider = StateProvider<String>((ref) => '');
final importFormatStateProvider =
StateProvider<ImportFormat>((ref) => ImportFormat.curl);
final userOnboardedProvider = StateProvider<bool>((ref) => false);

final workflowProvider = StateNotifierProvider<WorkflowNotifier, List<NodeData>>((ref) => WorkflowNotifier());
final hoverNodeProvider = StateProvider<int?>((ref) => null);
final connectionListProvider = StateProvider<List<Connection>>((ref) => []);
class WorkflowNotifier extends StateNotifier<List<NodeData>> {
WorkflowNotifier() : super([
NodeData(id: 1, offset: Offset(0, 0)),
NodeData(id: 2, offset: Offset(50, 50)),
]);

void updateNodeOffset(int id, Offset newOffset) {
state = [
for (final node in state)
if (node.id == id) node.copyWith(offset: newOffset) else node,
];
}

void addNode(NodeData node) {
state = [...state, node];
}

void updateNode(int id, {String? title, String? url, Map<String, String>? headers}) {
state = [
for (final node in state)
if (node.id == id)
node.copyWith(title: title, url: url, headers: headers)
else
node,
];
}
}

class ConnectionListNotifier extends StateNotifier<List<Connection>> {
ConnectionListNotifier() : super([]); // Initialize with an empty list

void addConnection(Connection connection) {
state = [...state, connection]; // Add a new connection
}

void removeConnection(Connection connection) {
state = state.where((c) => c != connection).toList(); // Remove a connection
}
}

26 changes: 26 additions & 0 deletions lib/screens/collections/collections.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:apidash/screens/home_page/collection_pane.dart';
import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart';
import 'package:apidash/screens/mobile/requests_page/requests_page.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:apidash/widgets/widgets.dart';

class CollectionPage extends StatelessWidget {
const CollectionPage({super.key});

@override
Widget build(BuildContext context) {
return context.isMediumWindow
? const RequestResponsePage()
: const Column(
children: [
Expanded(
child: DashboardSplitView(
sidebarWidget: CollectionPane(),
mainWidget: RequestEditorPane(),
),
),
],
);
}
}
59 changes: 59 additions & 0 deletions lib/screens/dashboard.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'package:apidash/screens/collections/collections.dart';
import 'package:apidash/screens/dashflow/dashflow.dart';
import 'package:apidash/screens/monitor/monitor.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -68,12 +71,65 @@ class Dashboard extends ConsumerWidget {
'History',
style: Theme.of(context).textTheme.labelSmall,
),
kVSpacer10,
IconButton(
isSelected: railIdx == 3,
onPressed: () {
ref.read(navRailIndexStateProvider.notifier).state = 3;
},
icon: const Icon(Icons.layers_sharp),
selectedIcon: const Icon(Icons.layers_outlined),
),
Text(
'Collections',
style: Theme.of(context).textTheme.labelSmall,
),
kVSpacer10,
IconButton(
isSelected: railIdx == 4,
onPressed: () {
ref.read(navRailIndexStateProvider.notifier).state = 4;
},
icon: const Icon(Icons.monitor_heart_sharp),
selectedIcon: const Icon(Icons.monitor_heart_outlined),
),
Text(
'Monitors',
style: Theme.of(context).textTheme.labelSmall,
),
kVSpacer10,
IconButton(
isSelected: railIdx == 5,
onPressed: () {
ref.read(navRailIndexStateProvider.notifier).state = 5;
},
icon: Image.asset("assets/images/dashflow_black.png",height: MediaQuery.of(context).size.height*0.05,),
selectedIcon: Image.asset("assets/images/dashflow_blue.png",height: MediaQuery.of(context).size.height*0.05,),
),
Text(
'Dash Flow',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: NavbarButton(
railIdx: railIdx,
selectedIcon: Icons.play_circle,
icon: Icons.play_circle_outlined,
label: 'Runner',
showLabel: false,
isCompact: true,
onTap: () {
showAboutAppDialog(context);
},
),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: NavbarButton(
Expand Down Expand Up @@ -118,6 +174,9 @@ class Dashboard extends ConsumerWidget {
HomePage(),
EnvironmentPage(),
HistoryPage(),
CollectionPage(),
MonitorPage(),
DashflowPage(),
SettingsPage(),
],
),
Expand Down
22 changes: 22 additions & 0 deletions lib/screens/dashflow/dashflow.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:apidash/screens/dashflow/dashflow_builder/dashflow_builder.dart';
import 'package:apidash/screens/dashflow/workflow_pane.dart';
import 'package:flutter/material.dart';
import 'package:apidash/widgets/widgets.dart';

class DashflowPage extends StatelessWidget {
const DashflowPage({super.key});

@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: DashboardSplitView(
sidebarWidget: WorkflowPane(),
mainWidget: DashflowBuilder(),
),
),
],
);
}
}
23 changes: 23 additions & 0 deletions lib/screens/dashflow/dashflow_builder/dashflow_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:apidash/screens/dashflow/dashflow_builder/workflow_canvas.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';

class DashflowBuilder extends ConsumerWidget {
const DashflowBuilder({
super.key,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
if (selectedId == null) {
return WorkflowCanvas();
} else {
return WorkflowCanvas();
}
}
}



74 changes: 74 additions & 0 deletions lib/screens/dashflow/dashflow_builder/grid.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vm;

class GridPainter extends CustomPainter {
final Matrix4 transformation;
final Size viewportSize;
final double baseGridSize;
final double canvasSize;

GridPainter({
required this.transformation,
required this.viewportSize,
this.baseGridSize = 20,
required this.canvasSize,
});
Offset _transformPoint(Offset point, Matrix4 matrix) {
final vector = matrix.transform3(vm.Vector3(point.dx, point.dy, 0));
return Offset(vector.x, vector.y);
}

@override
void paint(Canvas canvas, Size size) {
final zoom = transformation.getMaxScaleOnAxis();
final gridSize = baseGridSize * zoom;
final inverted = Matrix4.copy(transformation)..invert();

// Increase grid spacing at low zoom to reduce lines
final effectiveGridSize = zoom < 0.5
? gridSize * 4
: zoom < 1
? gridSize * 2
: gridSize;

// Center the origin
final centerOffset = Offset(canvasSize / 2, canvasSize / 2);
final topLeft = _transformPoint(Offset.zero, inverted) - centerOffset;
final bottomRight =
_transformPoint(Offset(size.width, size.height), inverted) -
centerOffset;

// Viewport-relative offsets
final startX =
(topLeft.dx ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble();
final endX = (bottomRight.dx ~/ effectiveGridSize + 1) *
effectiveGridSize.toDouble();

final startY =
(topLeft.dy ~/ effectiveGridSize - 1) * effectiveGridSize.toDouble();
final endY = (bottomRight.dy ~/ effectiveGridSize + 1) *
effectiveGridSize.toDouble();

final paint = Paint()
..color = Colors.blue.shade700
..style = PaintingStyle.fill;

// Draw dots at grid box corners
for (double x = startX; x <= endX; x += effectiveGridSize) {
for (double y = startY; y <= endY; y += effectiveGridSize) {
canvas.drawCircle(
Offset(x + centerOffset.dx, y + centerOffset.dy),
1.0,
paint,
);
}
}
}

@override
bool shouldRepaint(GridPainter oldDelegate) {
return transformation != oldDelegate.transformation ||
viewportSize != oldDelegate.viewportSize ||
canvasSize != oldDelegate.canvasSize;
}
}
87 changes: 87 additions & 0 deletions lib/screens/dashflow/dashflow_builder/node_connectors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'nodes.dart';

class ArrowPainter extends CustomPainter {
final List<NodeData> nodes;
final List<Connection> connections;
final double gridSize;
final double canvasSize;
final int? hoveredNodeId; // Properly declared as optional

ArrowPainter({
required this.nodes,
required this.connections,
required this.gridSize,
required this.canvasSize,
this.hoveredNodeId,
});

@override
void paint(Canvas canvas, Size size) {
if (nodes.length < 2) return;

// Find Node 1 and Node 2
final sourceNode = nodes.firstWhere((n) => n.id == 1);
final targetNode = nodes.firstWhere((n) => n.id == 2);
final isHighlighted = hoveredNodeId == 1 || hoveredNodeId == 2;

// Define paint style
final paint = Paint()
..color = isHighlighted ? Colors.blue : Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = isHighlighted ? 3 : 2;

for (var conn in connections) {
final fromNode = nodes.firstWhere((n) => n.id == conn.from, orElse: () => NodeData(id: -1, offset: Offset.zero));
final toNode = nodes.firstWhere((n) => n.id == conn.to, orElse: () => NodeData(id: -1, offset: Offset.zero));
if (fromNode.id == -1 || toNode.id == -1) continue;}


// Get rendered sizes using GlobalKey
final sourceRenderBox =
sourceNode.sizeKey.currentContext?.findRenderObject() as RenderBox?;
final targetRenderBox =
targetNode.sizeKey.currentContext?.findRenderObject() as RenderBox?;
if (sourceRenderBox == null || targetRenderBox == null) return;

final sourceSize = sourceRenderBox.size;
final targetSize = targetRenderBox.size;

// Define flexible connection points (edges of cards)
final sourceEdgeX =
sourceNode.offset.dx + canvasSize / 2 + sourceSize.width / 2;
final sourceEdgeY =
sourceNode.offset.dy + canvasSize / 2 + sourceSize.height / 2;
final targetEdgeX =
targetNode.offset.dx + canvasSize / 2 + targetSize.width / 2;
final targetEdgeY =
targetNode.offset.dy + canvasSize / 2 + targetSize.height / 2;

// Z-shaped path with two L-turns, snapped to grid
final midX = ((sourceEdgeX + targetEdgeX) / 2);
final pathPoints = [
Offset(sourceEdgeX, sourceEdgeY), // Right edge of source node
Offset(midX, sourceEdgeY), // Horizontal to midpoint
Offset(midX, targetEdgeY), // Vertical to target height
Offset(targetEdgeX, targetEdgeY), // Left edge of target node
];

// Draw the path
final path = Path()
..moveTo(pathPoints[0].dx, pathPoints[0].dy)
..lineTo(pathPoints[1].dx, pathPoints[1].dy)
..lineTo(pathPoints[2].dx, pathPoints[2].dy)
..lineTo(pathPoints[3].dx, pathPoints[3].dy);
canvas.drawPath(path, paint);

// Debug prints (optional, remove after testing)
// print('Snapped Source: $snappedSource');
// print('Middle Point: ${pathPoints[1]}');
// print('Snapped Target: $snappedTarget');
// print('Angle: $angle');
}

@override
bool shouldRepaint(ArrowPainter oldDelegate) =>
true;
}
Loading