diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4314849 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 55096e1..b857c1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ ARG WORKSPACE_HOST="https://localhost:8089" ARG WORKFLOW_HOST="https://localhost:8088" ARG SEARCH_HOST="https://localhost:49618" ARG ITEM_HOST="https://localhost:8087" +ARG SCHEDULER_HOST="http://localhost:8090" +ARG LOGS_HOST="http://localhost:3100" # define variables ARG FLUTTER_SDK=/usr/local/flutter diff --git a/lib/core/models/workspace_local.dart b/lib/core/models/workspace_local.dart index 9d40964..f143a18 100644 --- a/lib/core/models/workspace_local.dart +++ b/lib/core/models/workspace_local.dart @@ -34,6 +34,7 @@ class WorkspaceLocal { _service.put(context, ws.id!, { "active" : true }, {}); } if (ws.active == true && changeCurrent) { + print(ws.serialize()); current = ws.id; } fill(); @@ -95,7 +96,13 @@ class WorkspaceLocal { }); } + static void changeWorkspaceByName(String name) { + var id = workspaces.entries.firstWhere((element) => element.value.name == "${name}_workspace").key; + changeWorkspace(id); + } + static void changeWorkspace(String id) { + _service.put(null, id, { "active" : true }, {}); current = id; fill(); endDrawerKey.currentState?.setState(() {}); diff --git a/lib/core/sections/header/search.dart b/lib/core/sections/header/search.dart index 46ed536..47749c9 100644 --- a/lib/core/sections/header/search.dart +++ b/lib/core/sections/header/search.dart @@ -61,7 +61,7 @@ class SearchWidgetState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - width: MediaQuery.of(context).size.width - 400 > 0 ? MediaQuery.of(context).size.width - 300 - 100 : MediaQuery.of(context).size.width, + width: MediaQuery.of(context).size.width - 400 > 0 ? MediaQuery.of(context).size.width - 300 - 100 : 200, height: 50, color: Colors.white, child: TextField( diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 40d564c..8d88e2f 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -74,15 +74,10 @@ class APIService { Future> _main(String url, dynamic body, String method, String succeed, bool force, BuildContext? context, Options? options) async { var err = ""; - - if ((!force) && cache.containsKey(url) && cache[url] != null ) { - return cache[url]! as APIResponse; - } try { _dio.options.headers["authorization"] = auth; _dio.interceptors.clear(); var response = await _request(url, method, body, options); - print(response); if (response.statusCode != null && response.statusCode! < 400) { if (method == "delete") { cache.remove(url); return APIResponse(); } APIResponse resp = APIResponse().deserialize(response.data); @@ -117,6 +112,7 @@ class APIService { try { _dio.options.headers["authorization"] = auth; _dio.interceptors.clear(); + var response = await _request(url, method, body, null); if (response.statusCode != null && response.statusCode! < 400) { if (method == "delete") { cache.remove(url); return APIResponse(); } diff --git a/lib/core/services/specialized_services/check_service.dart b/lib/core/services/specialized_services/check_service.dart new file mode 100644 index 0000000..d82f9fe --- /dev/null +++ b/lib/core/services/specialized_services/check_service.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:oc_front/core/services/api_service.dart'; +import 'package:oc_front/core/services/specialized_services/abstract_service.dart'; +import 'package:oc_front/models/response.dart'; +import 'package:oc_front/models/workflow.dart'; + +class CheckService extends AbstractService { + @override APIService service = APIService( + baseURL: const String.fromEnvironment('WORKFLOW_HOST', defaultValue: 'http://localhost:8088') + ); + @override String subPath = "/oc/workflow/check/"; + + Future> search(BuildContext? context, List words, Map params) { + return service.get("$subPath${words.join("/")}", true, context); + } + @override + Future> all(BuildContext? context) { + throw UnimplementedError(); + } + @override + Future> get(BuildContext? context, String id) { + throw UnimplementedError(); + } + @override + Future> post(BuildContext? context, Map body, Map params) { + throw UnimplementedError(); + } + @override + Future> put(BuildContext? context, String id, Map body, Map params) { + throw UnimplementedError(); + } + @override + Future> delete(BuildContext? context, String id, Map params) { + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/core/services/specialized_services/item_service.dart b/lib/core/services/specialized_services/item_service.dart deleted file mode 100644 index 61811be..0000000 --- a/lib/core/services/specialized_services/item_service.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:oc_front/core/services/api_service.dart'; -import 'package:oc_front/core/services/specialized_services/abstract_service.dart'; -import 'package:oc_front/models/abstract.dart'; -import 'package:oc_front/models/search.dart'; - -class ItemService> extends AbstractService { - @override APIService service = APIService( - baseURL: const String.fromEnvironment('ITEM_HOST', defaultValue: 'http://localhost:8087') - ); - @override String subPath = "/oc/${getTopic(S)}/"; -} \ No newline at end of file diff --git a/lib/core/services/specialized_services/logs_service.dart b/lib/core/services/specialized_services/logs_service.dart index 4b4fb96..0156392 100644 --- a/lib/core/services/specialized_services/logs_service.dart +++ b/lib/core/services/specialized_services/logs_service.dart @@ -4,31 +4,31 @@ import 'package:oc_front/core/services/specialized_services/abstract_service.dar import 'package:oc_front/models/logs.dart'; import 'package:oc_front/models/response.dart'; -class LogsService extends AbstractService { - @override APIService service = APIService( - baseURL: const String.fromEnvironment('SCHEDULER_HOST', defaultValue: 'http://localhost:3100') +class LogsService extends AbstractService { + @override APIService service = APIService( + baseURL: const String.fromEnvironment('LOGS_HOST', defaultValue: 'http://localhost:3100') ); @override String subPath = "/loki/api/v1/"; - @override Future> search(BuildContext? context, List words, Map params) { + @override Future> search(BuildContext? context, List words, Map params) { List v = []; for (var p in params.keys) { if (p == "start" || p == "end") { continue; } v.add("$p=\"${params[p]}\""); } - return service.get("${subPath}query_range?query={${v.join(", ")}}&start=${params["start"]}&end=${params["end"]}", false, context); + return service.get("${subPath}query_range?query={${v.join(", ")}}&start=${params["start"].toString().substring(0, 10)}&end=${params["end"].toString().substring(0, 10)}", false, context); } - @override Future> get(BuildContext? context, String id) { + @override Future> get(BuildContext? context, String id) { throw UnimplementedError(); } - @override Future> post(BuildContext? context, Map body, Map params) { + @override Future> post(BuildContext? context, Map body, Map params) { throw UnimplementedError(); } - @override Future> put(BuildContext? context, String id, Map body, Map params) { + @override Future> put(BuildContext? context, String id, Map body, Map params) { throw UnimplementedError(); } - @override Future> delete(BuildContext? context, String id, Map params) { + @override Future> delete(BuildContext? context, String id, Map params) { throw UnimplementedError(); } } diff --git a/lib/models/logs.dart b/lib/models/logs.dart index 5b86fb9..4617927 100644 --- a/lib/models/logs.dart +++ b/lib/models/logs.dart @@ -1,32 +1,6 @@ import 'package:oc_front/models/abstract.dart'; import 'package:json_string/json_string.dart'; -class LogResults extends SerializerDeserializer { - String? status; - LogsResult? data; - - LogResults({ - this.status, - this.data, - }); - - String getID() { - return ""; - } - - @override deserialize(dynamic json) { - try { json = json as Map; - } catch (e) { return LogResults(); } - return LogResults( - status: json.containsKey("status") ? json["status"] : "", - data: json.containsKey("data") ? LogsResult().deserialize(json["data"]) : null, - ); - } - @override Map serialize() { - return { }; - } -} - class LogsResult extends SerializerDeserializer { List result; LogsResult({ @@ -45,7 +19,9 @@ class LogsResult extends SerializerDeserializer { ); } @override Map serialize() { - return { }; + return { + "result": toListJson(result), + }; } } @@ -54,6 +30,7 @@ class Logs extends SerializerDeserializer { List logs = []; Logs({ this.level, + this.logs = const [], }); String getID() { @@ -65,11 +42,13 @@ class Logs extends SerializerDeserializer { } catch (e) { return Logs(); } return Logs( level: json.containsKey("stream") && (json["stream"] as Map).containsKey("level") ? json["stream"]["level"] : "", - + logs: json.containsKey("values") ? fromListJson(json["values"], Log()) : [], ); } @override Map serialize() { - return { }; + return { + "level": level, + }; } } @@ -78,10 +57,12 @@ class Log extends SerializerDeserializer { String? message; String? level; + String? rawMessage; Map map = {}; Log({ this.timestamp, this.message, + this.rawMessage, this.level }); @@ -97,7 +78,7 @@ class Log extends SerializerDeserializer { if (j["Status"] == "Pending") { jsonString = "${j["Name"]} : [${j["Namespace"]}] Status: ${j["Status"]}... \nCreated at ${j["Created"].toString().replaceAllMapped(RegExp(r'\(\w+\)'), (match) { return ''; }).replaceAllMapped(RegExp(r'\+\w+'), (match) { return ''; })}"; } else { - jsonString = "${j["Name"]} : [${j["Namespace"]}] ${j["Status"]} ${j["Progress"]} (${j["Duration"].toString().replaceAll("seconds", "s")})\nStarted at ${j["Created"].toString().replaceAllMapped(RegExp(r'\(\w+\)'), (match) { return ''; }).replaceAllMapped(RegExp(r'\+\w+'), (match) { return ''; })}"; + jsonString = "${j["Name"]} : [${j["Namespace"]}] ${j["Status"]} ${j["Progress"]} (${j["Duration"].toString()})\nCreated at ${j["Created"].toString().replaceAllMapped(RegExp(r'\(\w+\)'), (match) { return ''; }).replaceAllMapped(RegExp(r'\+\w+'), (match) { return ''; })}; Started at ${j["Created"].toString().replaceAllMapped(RegExp(r'\(\w+\)'), (match) { return ''; }).replaceAllMapped(RegExp(r'\+\w+'), (match) { return ''; })}"; } } on JsonFormatException catch (e) { /* */ } message = jsonString; @@ -107,10 +88,13 @@ class Log extends SerializerDeserializer { @override deserialize(dynamic json) { try { json = json as List; } catch (e) { return Log(); } - return Log( - timestamp: json.isNotEmpty ? DateTime.parse(json[0]) : null, + var l = Log( + timestamp: json.isNotEmpty ? DateTime.fromMillisecondsSinceEpoch(int.parse(json[0]) ~/ 1000) : null, message: json.length > 1 ? getMessage(json[1].toString()) : null, + rawMessage : json.length > 1 ? json[1].toString() : null, ); + l.getMessage(l.message ?? ""); + return l; } @override Map serialize() { return { }; } } \ No newline at end of file diff --git a/lib/models/response.dart b/lib/models/response.dart index 3283a3c..92250a9 100644 --- a/lib/models/response.dart +++ b/lib/models/response.dart @@ -15,7 +15,8 @@ Map refs = { Workflow: Workflow(), Resource: Resource(), WorkflowExecutions: WorkflowExecutions(), - LogResults: LogResults(), + LogsResult: LogsResult(), + Check: Check(), }; class APIResponse { diff --git a/lib/models/search.dart b/lib/models/search.dart index 09359e0..8e8e675 100644 --- a/lib/models/search.dart +++ b/lib/models/search.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; import 'package:flutter_flow_chart/flutter_flow_chart.dart'; import 'package:oc_front/models/abstract.dart'; @@ -248,10 +251,21 @@ class ProcessingItem extends SerializerDeserializer implements A @override String getName() { return name ?? ""; } + double? width; + double? height; + @override + double? getWidth() { + return width; + } + @override + double? getHeight() { + return height; + } + @override deserialize(dynamic json) { try { json = json as Map; } catch (e) { return ProcessingItem(); } - return ProcessingItem( + var w = ProcessingItem( id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : null, logo: json.containsKey("logo") ? json["logo"] : null, @@ -274,6 +288,19 @@ class ProcessingItem extends SerializerDeserializer implements A scallingModel: json.containsKey("scalling_model") ? json["scalling_model"] : null, diskIO: json.containsKey("disk_io") ? json["disk_io"] : null, ); + if (w.logo != null) { + // + var image = Image.network(w.logo!); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + w.width = info.image.width.toDouble(); + w.height = info.image.height.toDouble(); + })); + } + return w; } @override Map serialize() => { "id": id, @@ -336,13 +363,24 @@ class WorkflowItem extends SerializerDeserializer implements Abstr return id ?? ""; } + double? width; + double? height; + @override + double? getWidth() { + return width; + } + @override + double? getHeight() { + return height; + } + @override String getName() { return name ?? ""; } @override deserialize(dynamic json) { try { json = json as Map; } catch (e) { return WorkflowItem(); } - return WorkflowItem( + var w = WorkflowItem( id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : null, logo: json.containsKey("logo") ? json["logo"] : null, @@ -358,6 +396,19 @@ class WorkflowItem extends SerializerDeserializer implements Abstr model: json.containsKey("resource_model") ? ResourceModel().deserialize(json["resource_model"]) : null, workflowID: json.containsKey("workflow_id") ? json["workflow_id"] : null, ); + if (w.logo != null) { + // + var image = Image.network(w.logo!); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + w.width = info.image.width.toDouble(); + w.height = info.image.height.toDouble(); + })); + } + return w; } @override Map serialize() => { "id": id, @@ -420,10 +471,21 @@ class DataItem extends SerializerDeserializer implements AbstractItem< @override String getID() { return id ?? ""; } + + double? width; + double? height; + @override + double? getWidth() { + return width; + } + @override + double? getHeight() { + return height; + } @override deserialize(dynamic json) { try { json = json as Map; } catch (e) { return DataItem(); } - return DataItem( + var w = DataItem( id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : null, logo: json.containsKey("logo") ? json["logo"] : null, @@ -441,6 +503,19 @@ class DataItem extends SerializerDeserializer implements AbstractItem< dataType: json.containsKey("data_type") ? json["data_type"] : null, exemple: json.containsKey("exemple") ? json["exemple"] : null, ); + if (w.logo != null) { + // + var image = Image.network(w.logo!); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + w.width = info.image.width.toDouble(); + w.height = info.image.height.toDouble(); + })); + } + return w; } @override Map serialize() => { "id": id, @@ -505,10 +580,21 @@ class DataCenterItem extends SerializerDeserializer implements A @override String getName() { return name ?? ""; } + double? width; + double? height; + @override + double? getWidth() { + return width; + } + @override + double? getHeight() { + return height; + } @override deserialize(dynamic json) { try { json = json as Map; } catch (e) { return DataCenterItem(); } - return DataCenterItem( + + var w = DataCenterItem( id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : null, logo: json.containsKey("logo") ? json["logo"] : null, @@ -526,6 +612,19 @@ class DataCenterItem extends SerializerDeserializer implements A gpus: json.containsKey("gpus") ? fromListJson(json["gpus"], GPU()) : [], ram: json.containsKey("ram") ? RAM().deserialize(json["ram"]) : null, ); + if (w.logo != null) { + // + var image = Image.network(w.logo!); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + w.width = info.image.width.toDouble(); + w.height = info.image.height.toDouble(); + })); + } + return w; } @override Map serialize() => { "id": id, @@ -680,10 +779,20 @@ class StorageItem extends SerializerDeserializer implements Abstrac @override String getID() { return id ?? ""; } + double? width; + double? height; + @override + double? getWidth() { + return width; + } + @override + double? getHeight() { + return height; + } @override deserialize(dynamic json) { try { json = json as Map; } catch (e) { return StorageItem(); } - return StorageItem( + var w = StorageItem( id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : null, logo: json.containsKey("logo") ? json["logo"] : null, @@ -705,6 +814,19 @@ class StorageItem extends SerializerDeserializer implements Abstrac redundancy: json.containsKey("redundancy") ? json["redundancy"] : null, throughput: json.containsKey("throughput") ? json["throughput"] : null, ); + if (w.logo != null) { + // + var image = Image.network(w.logo!); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + w.width = info.image.width.toDouble(); + w.height = info.image.height.toDouble(); + })); + } + return w; } @override Map serialize() => { "id": id, diff --git a/lib/models/workflow.dart b/lib/models/workflow.dart index 23226bc..7f4681d 100644 --- a/lib/models/workflow.dart +++ b/lib/models/workflow.dart @@ -3,8 +3,31 @@ import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_flow_chart/flutter_flow_chart.dart'; import 'package:oc_front/core/models/workspace_local.dart'; import 'package:oc_front/models/abstract.dart'; +import 'package:oc_front/models/logs.dart'; import 'package:oc_front/models/search.dart'; +class Check extends SerializerDeserializer { + bool is_available = false; + + Check({ + this.is_available = false, + }); + + @override deserialize(dynamic json) { + try { json = json as Map; + } catch (e) { return Check(); } + return Check( + is_available: json.containsKey("is_available") ? json["is_available"] : false, + ); + } + @override Map serialize() { + return { + "is_available": is_available, + }; + } +} + + class WorkflowExecutions extends SerializerDeserializer { List executions = []; String? executionData; @@ -36,6 +59,8 @@ class WorkflowExecution extends SerializerDeserializer { String? endDate; int? status; String? workflowId; + + List? logs; WorkflowExecution({ @@ -83,6 +108,7 @@ class Workflow extends SerializerDeserializer { List workflows; Graph? graph; Scheduler? schedule; + bool scheduleActive = false; Workflow({ this.id, @@ -94,6 +120,7 @@ class Workflow extends SerializerDeserializer { this.workflows = const [], this.graph, this.schedule, + this.scheduleActive = false, }); String getID() { @@ -110,6 +137,7 @@ class Workflow extends SerializerDeserializer { processing: json.containsKey("processings") ? json["processings"] : [], datacenter: json.containsKey("datacenters") ? json["datacenters"] : [], data: json.containsKey("datas") ? json["datas"] : [], + scheduleActive: json.containsKey("schedule_active") ? json["schedule_active"] : false, storage: json.containsKey("storages") ? json["storages"] : [], graph: json.containsKey("graph") ? Graph().deserialize(json["graph"]) : null, schedule: json.containsKey("schedule") ? Scheduler().deserialize(json["schedule"]) : null, @@ -124,6 +152,7 @@ class Workflow extends SerializerDeserializer { "storages": storage, "processings": processing, "workflows": workflows, + "schedule_active": scheduleActive, "schedule": schedule?.serialize(), }; if (graph != null) { @@ -135,6 +164,7 @@ class Workflow extends SerializerDeserializer { void fromDashboard(Map j) { id = j["id"]; name = j["name"]; + scheduleActive = j["schedule_active"]; if (j.containsKey("graph")) { graph = Graph(); graph!.fromDashboard(j["graph"]); @@ -149,6 +179,7 @@ class Workflow extends SerializerDeserializer { "id": id, "name": name, "graph": graph?.toDashboard(), + "schedule_active": scheduleActive, "schedule": schedule?.toDashboard(), }; } @@ -160,13 +191,15 @@ class Scheduler extends SerializerDeserializer { String? cron; DateTime? start; DateTime? end; + int? mode; Scheduler({ this.id, this.name, this.cron, this.start, - this.end + this.end, + this.mode, }); void fromDashboard(Map j) { @@ -177,13 +210,14 @@ class Scheduler extends SerializerDeserializer { if (j.containsKey("end") && j["end"] != null) { end = DateTime.parse(j["end"]); } - + mode = int.parse(j["mode"].toString()); } Map toDashboard() { return { "id": id, "name": name, "cron": cron, + "mode": int.parse(mode.toString()), "start": start?.toIso8601String(), "end": end?.toIso8601String(), }; @@ -196,6 +230,7 @@ class Scheduler extends SerializerDeserializer { id: json.containsKey("id") ? json["id"] : null, name: json.containsKey("name") ? json["name"] : "", cron: json.containsKey("cron") ? json["cron"] : "", + mode: json.containsKey("mode") ? json["mode"] : "", start: json.containsKey("start") ? DateTime.parse(json["start"]) : null, end: json.containsKey("end") ? DateTime.parse(json["end"]) : null, ); @@ -204,6 +239,7 @@ class Scheduler extends SerializerDeserializer { "id": id, "name": name, "cron": cron ?? "", + "mode": int.parse(mode.toString()), "start": start?.toIso8601String(), "end": end?.toIso8601String(), }; diff --git a/lib/pages/scheduler.dart b/lib/pages/scheduler.dart index 415c9f2..3ee4350 100644 --- a/lib/pages/scheduler.dart +++ b/lib/pages/scheduler.dart @@ -2,6 +2,7 @@ import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart' as intl; import 'package:go_router/go_router.dart'; +import 'package:oc_front/core/services/specialized_services/logs_service.dart'; import 'package:oc_front/core/services/specialized_services/workflow_execution_service.dart'; import 'package:oc_front/models/workflow.dart'; import 'package:oc_front/pages/abstract_page.dart'; @@ -35,6 +36,7 @@ class SchedulerPageWidgetState extends State { "${widget.end.year}-${widget.end.month > 9 ? widget.end.month : "0${widget.end.month}"}-${widget.end.day > 9 ? widget.end.day : "0${widget.end.day}"}"], {}), builder: (ctx, as) { Map> data = {}; + if (as.hasData && as.data!.data != null) { for (var element in as.data!.data!.executions) { if (element.executionData == null) { continue; } @@ -47,6 +49,49 @@ class SchedulerPageWidgetState extends State { } } GlobalKey k = GlobalKey(); + for (var da in data.keys) { + for (var exec in data[da]!) { + String start = ""; + String end = ""; + try { + if (exec.endDate != null && exec.endDate != "") { + var startD = DateTime.parse(exec.executionData!); + var endD = DateTime.parse(exec.endDate!); + var diff = endD.difference(startD); + if (diff.inDays < 30) { + var rest = ((30 - diff.inDays) ~/ 2) - 1; + start = (startD.subtract(Duration(days: rest)).microsecondsSinceEpoch).toString(); + end = (endD.add(Duration(days: rest)).microsecondsSinceEpoch).toString(); + } else { + start = (startD.microsecondsSinceEpoch).toString(); + end = (startD.add( const Duration(days: 29)).microsecondsSinceEpoch).toString(); + } + } else { + start = (DateTime.parse(exec.executionData!).subtract( const Duration(days: 14)).microsecondsSinceEpoch).toString(); + end = (DateTime.parse(exec.executionData!).add( const Duration(days: 14)).microsecondsSinceEpoch).toString(); + } + } catch(e) { /* */ } + k.currentState?.setState(() { k.currentState?.widget.loading = true; }); + LogsService().search(context, [], { + "workflow_execution_id": exec.id, + "start": start, + "end": end + }).then((value) { + if (value.data != null) { + var d = value.data!; + for( var r in d.result) { + for (var element in r.logs) { + element.level = r.level; + exec.logs ??= []; + exec.logs!.add(element); + } + exec.logs?.sort((a, b) => a.timestamp!.compareTo(b.timestamp!)); + } + } + k.currentState?.setState(() { k.currentState?.widget.loading = false; }); + }); + } + } return Column( children: [ Container( color: const Color.fromRGBO(38, 166, 154, 1), height: 50, width: MediaQuery.of(context).size.width, @@ -69,7 +114,7 @@ class SchedulerPageWidgetState extends State { validator: (value) { return null; }, - resetIcon: const Icon(Icons.close, size: 15), + resetIcon: null, onShowPicker: (context, currentValue) async { var date = await showDatePicker( builder: (BuildContext context, Widget? child) { @@ -94,6 +139,7 @@ class SchedulerPageWidgetState extends State { ); return date; }, + format: intl.DateFormat('y-M-dd hh:mm:ss'), initialValue: widget.start, onChanged: (value) { @@ -126,7 +172,7 @@ class SchedulerPageWidgetState extends State { validator: (value) { return null; }, - resetIcon: const Icon(Icons.close, size: 15), + resetIcon: null, onShowPicker: (context, currentValue) async { var date = await showDatePicker( builder: (BuildContext context, Widget? child) { @@ -176,7 +222,7 @@ class SchedulerPageWidgetState extends State { ) ])) ), - ScheduleWidget( key: k, data: data, start: widget.start, end : widget.end, isList: widget.isList ) + ScheduleWidget( key: k, data: data, start: widget.start, end : widget.end, isList: widget.isList, ) ]); }); } diff --git a/lib/pages/workflow.dart b/lib/pages/workflow.dart index 3f36c84..29acee7 100644 --- a/lib/pages/workflow.dart +++ b/lib/pages/workflow.dart @@ -22,6 +22,7 @@ class WorkflowFactory implements AbstractFactory { @override Widget factory(GoRouterState state, List args) { return WorkflowPageWidget(); } @override void search(BuildContext context) { } } +bool getAll = true; class WorkflowPageWidget extends StatefulWidget { WorkflowPageWidget () : super(key: WorkflowFactory.key); @@ -107,7 +108,6 @@ final WorflowService _service = WorflowService(); item.position?.x = (item.position?.x ?? 0) + 52.5; item.position?.y = (item.position?.y ?? 0) + 52.5; } - print(dash.getZoomFactor()); updateW.graph?.zoom = dash.getZoomFactor(); await _service.put(context, id, updateW.serialize(), {}); } @@ -128,7 +128,9 @@ final WorflowService _service = WorflowService(); Widget menuExtension() { var quart = MediaQuery.of(context).size.width / 6; return MenuWorkspaceWidget(simpliest: true, width: quart > 80 ? quart : 80, - onWorkspaceChange: () { dash.selectedLeftMenuKey.currentState?.setState(() { }); }); + onWorkspaceChange: () { + dash.selectedLeftMenuKey.currentState?.setState(() { }); + }); } Widget onDashboardAlertOpened(BuildContext context, Dashboard dash) { @@ -148,7 +150,7 @@ final WorflowService _service = WorflowService(); dashboard: dash, itemWidget: itemBuild, categories: const ["processing", "data", "datacenter", "storage", "workflows"], - draggableItemBuilder: (cat) => WorkspaceLocal.byTopic(cat, true).toList(), + draggableItemBuilder: (cat) => WorkspaceLocal.byTopic(cat, false), itemWidgetTooltip: itemTooltipBuild, innerMenuWidth: quart > 80 ? quart : 80, menuExtension: menuExtension, diff --git a/lib/widgets/dialog/new_box.dart b/lib/widgets/dialog/new_box.dart index 876f9bd..d5988ec 100644 --- a/lib/widgets/dialog/new_box.dart +++ b/lib/widgets/dialog/new_box.dart @@ -113,11 +113,11 @@ class NewBoxWidgetState> extends State ? MouseCursor.defer : SystemMouseCursors.click, onTap: () async { if (widget._selected == null || widget._selected!.isEmpty) { return; } - if (widget._selected != null && widget.dash.load != null) { - await widget.dash.load!(widget._selected ?? ""); - WorkspaceLocal.init(context, true); - } widget.dash.isOpened = true; + if (widget._selected != null && widget.dash.load != null) { + WorkspaceLocal.changeWorkspaceByName(widget._selected!.split("~")[1]); + await widget.dash.load!(widget._selected ?? ""); + } widget.dash.notifyListeners(); Navigator.pop(context); }, diff --git a/lib/widgets/forms/scheduler_forms.dart b/lib/widgets/forms/scheduler_forms.dart index 9b8eabc..147ba19 100644 --- a/lib/widgets/forms/scheduler_forms.dart +++ b/lib/widgets/forms/scheduler_forms.dart @@ -1,20 +1,46 @@ +import 'package:alert_banner/exports.dart'; import 'package:cron/cron.dart'; import 'package:intl/intl.dart' as intl; import 'package:flutter/material.dart'; import 'package:flutter_flow_chart/flutter_flow_chart.dart'; import 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; import 'package:datetime_picker_formfield/datetime_picker_formfield.dart'; +import 'package:oc_front/core/services/specialized_services/check_service.dart'; +import 'package:oc_front/pages/workflow.dart'; +import 'package:oc_front/widgets/dialog/alert.dart'; class SchedulerFormsWidget extends StatefulWidget { Dashboard item; String purpose = ""; + bool? booking; Function validate = () {}; - SchedulerFormsWidget ({ super.key, required this.item }); + SchedulerFormsWidget ({ super.key, required this.item, }); @override SchedulerFormsWidgetState createState() => SchedulerFormsWidgetState(); } class SchedulerFormsWidgetState extends State { + CheckService check = CheckService(); @override Widget build(BuildContext context) { - if (widget.item.schedulerState["service"] == null) { widget.item.schedulerState["service"] = true; } + try { + if (widget.item.scheduler["mode"] == null) { widget.item.scheduler["mode"] = 1; } + } catch (e) { + widget.item.scheduler = { "mode": 1 }; + } + DateTime? start; + DateTime? end; + if (widget.item.scheduler["start"] != null) { + start = DateTime.parse(widget.item.scheduler["start"]!); + if (start.isBefore(DateTime.now()) && !dash.scheduleActive) { + start = DateTime.now().add(const Duration(minutes: 5)); + widget.item.scheduler["start"] = start.toUtc().toIso8601String(); + } + } + if (widget.item.scheduler["end"] != null) { + end = DateTime.parse(widget.item.scheduler["end"]!); + if (end.isBefore(DateTime.now()) && !dash.scheduleActive) { + end = DateTime.now().add(const Duration(minutes: 5)); + widget.item.scheduler["end"] = end.toUtc().toIso8601String(); + } + } List> formKeys = [GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey()]; return Column( children: [ @@ -27,7 +53,7 @@ class SchedulerFormsWidgetState extends State { Container(height: 20), AdvancedSwitch( width: 140, - initialValue: widget.item.schedulerState["service"] == true, + initialValue: widget.item.scheduler["mode"] == 1, activeColor: Colors.green, inactiveColor: Colors.green, activeChild: const Text("service", style: TextStyle(color: Colors.white)), inactiveChild: const Text("cron task", style: TextStyle(color: Colors.white)), @@ -35,8 +61,8 @@ class SchedulerFormsWidgetState extends State { onChanged: (value) { Future.delayed(const Duration(milliseconds: 100), () => setState(() { - widget.item.schedulerState["service"] = value; - if ((widget.item.schedulerState["service"] == true )) { widget.item.scheduler.remove("cron"); } + widget.item.scheduler["mode"] = value == true ? 1 : 0; + if ((widget.item.scheduler["mode"] == 1 )) { widget.item.scheduler.remove("cron"); } })); },), Container(height: 5), @@ -44,12 +70,18 @@ class SchedulerFormsWidgetState extends State { child: Container( height: 40, margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: TextFormField( key: formKeys[0], - initialValue: "${widget.item.schedulerState["service"] == true ? "" : "cron_"}${widget.item.scheduler["name"] ?? "${widget.item.name}_event"}", + initialValue: "${widget.item.scheduler["mode"] == 1 ? "" : "cron_"}${widget.item.scheduler["name"] ?? "${widget.item.name}_event"}", + enabled: !dash.scheduleActive, onChanged: (value) { + Future.delayed(const Duration(seconds: 100), () { + if (widget.item.scheduler["name"] == value) { + widget.item.save!(widget.item.id); + } + }); widget.item.scheduler["name"] = value; }, onSaved: (value) { - widget.item.scheduler["name"] = value ?? "${widget.item.schedulerState["service"] == true ? "" : "cron_"}${widget.item.scheduler["name"] ?? "${widget.item.name}_event"}"; + widget.item.scheduler["name"] = value ?? "${widget.item.scheduler["mode"] == 1 ? "" : "cron_"}${widget.item.scheduler["name"] ?? "${widget.item.name}_event"}"; }, validator: (value) => value == null || value.isEmpty ? "not empty" : null, style: const TextStyle(fontSize: 12), @@ -72,10 +104,8 @@ class SchedulerFormsWidgetState extends State { child: Container( height: 40, margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: DateTimeField( key: formKeys[1], - resetIcon: const Icon(Icons.close, size: 15), - onSaved: (value) { - widget.item.scheduler["start"] = "${(value ?? DateTime.now()).toIso8601String()}Z"; - }, + enabled: !dash.scheduleActive, + resetIcon: null, onShowPicker: (context, currentValue) async { var date = await showDatePicker( builder: (BuildContext context, Widget? child) { @@ -94,38 +124,53 @@ class SchedulerFormsWidgetState extends State { return w; }, context: context, - firstDate: DateTime(1900), - initialDate: DateTime.parse(widget.item.scheduler["start"] ?? currentValue?.toIso8601String() ?? ""), + firstDate: dash.scheduleActive ? DateTime(1900) : DateTime.now().add(const Duration(minutes: 5)), + initialDate: DateTime.parse( start?.toLocal().toIso8601String() + ?? currentValue?.toIso8601String() + ?? DateTime.now().add(const Duration(minutes: 5)).toUtc().toIso8601String()).toLocal(), lastDate: DateTime(2100) ); if (date != null) { - var time = await showTimePicker(context: context, - initialTime: TimeOfDay(hour: date.hour, minute: date.minute), - builder: (BuildContext context, Widget? child) { - Widget w = Theme( - data: ThemeData( - cardTheme: CardTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), - dialogTheme: DialogTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), - colorScheme: ColorScheme.light( - background: Colors.grey.shade300, - tertiary: Colors.grey, - secondary: Colors.grey, - primary: Colors.black), - ), - child: child ?? Container(), + var n = TimeOfDay.now(); + TimeOfDay? time = n; + var count = 0; + while(((time?.hour ?? 0) + ((time?.minute ?? 0) / 100)) <= (n.hour + ((n.minute + 1) / 100)) ) { + if (count > 0 && time != null) { + showAlertBanner( context, () {}, + const AlertAlertBannerChild( + text: "must be at least 1 minute from now to let system check info"),// <-- Put any widget here you want! + alertBannerLocation: AlertBannerLocation.bottom,); + } + time = await showTimePicker(context: context, + initialTime: TimeOfDay(hour: date.hour, minute: date.minute), + builder: (BuildContext context, Widget? child) { + Widget w = Theme( + data: ThemeData( + cardTheme: CardTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), + dialogTheme: DialogTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), + colorScheme: ColorScheme.light( + background: Colors.grey.shade300, + tertiary: Colors.grey, + secondary: Colors.grey, + primary: Colors.black), + ), + child: child ?? Container(), + ); + return w; + }, ); - return w; - }, - ); - date = date.add(Duration(hours: time?.hour ?? 0, minutes: time?.minute ?? 0)); + if (time == null) { return DateTime.now().add( const Duration(minutes: 1)); } + count++; + } + date = date.add(Duration(hours: time?.hour ?? 0, minutes: time?.minute ?? 0)); + widget.item.scheduler["start"] = date.toUtc().toIso8601String(); + widget.item.save!(widget.item.id); } return date; }, - format: intl.DateFormat('y-M-dd hh:mm:ss'), - initialValue: DateTime.parse(widget.item.scheduler["start"] ?? DateTime.now().toIso8601String()), - onChanged: (value) { - widget.item.scheduler["start"] = "${(value ?? DateTime.now()).toIso8601String()}Z"; - }, + format: intl.DateFormat('y-M-dd HH:mm:ss'), + initialValue: start?.toLocal() ?? DateTime.now(), + onChanged: (value) { }, validator: (value) => value == null ? "not empty" : null, style: const TextStyle(fontSize: 12), decoration: const InputDecoration( @@ -147,8 +192,14 @@ class SchedulerFormsWidgetState extends State { child: Container( height: 40, margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: DateTimeField( key: formKeys[2], + enabled: !dash.scheduleActive, validator: (value) { - return value == null && !(widget.item.schedulerState["service"] == true ) ? "not empty" : null; + return value == null && !(widget.item.scheduler["mode"] == 1 ) ? "not empty" : null; + }, + onChanged: (value) { + if (value == null) { + widget.item.scheduler.remove("end"); + } }, resetIcon: const Icon(Icons.close, size: 15), onShowPicker: (context, currentValue) async { @@ -169,45 +220,52 @@ class SchedulerFormsWidgetState extends State { return w; }, context: context, - firstDate: DateTime(1900), - initialDate: DateTime.parse(widget.item.scheduler["start"] ?? currentValue?.toIso8601String() ?? ""), + firstDate: dash.scheduleActive ? DateTime(1900) : DateTime.now().add(const Duration(minutes: 5)), + initialDate: DateTime.parse( end?.toLocal().toIso8601String() + ?? currentValue?.toIso8601String() + ?? DateTime.now().add(const Duration(minutes: 5)).toUtc().toIso8601String()).toLocal(), lastDate: DateTime(2100) ); if (date != null) { - var time = await showTimePicker(context: context, - initialTime: TimeOfDay(hour: date.hour, minute: date.minute), - builder: (BuildContext context, Widget? child) { - Widget w = Theme( - data: ThemeData( - cardTheme: CardTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), - dialogTheme: DialogTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), - colorScheme: ColorScheme.light( - background: Colors.grey.shade300, - tertiary: Colors.grey, - secondary: Colors.grey, - primary: Colors.black), - ), - child: child ?? Container(), + // ignore: use_build_context_synchronously + var n = TimeOfDay.now(); + TimeOfDay? time = TimeOfDay(hour: date.hour, minute: date.minute); + var count = 0; + while(((time?.hour ?? 0) + (time?.minute ?? 0 / 100)) <= (n.hour + ((n.minute + 1) / 100)) ) { + if (count > 0 && time != null) { + showAlertBanner( context, () {}, + const AlertAlertBannerChild(text: "must be at least 1 minute from now to let system check info"),// <-- Put any widget here you want! + alertBannerLocation: AlertBannerLocation.bottom,); + } + time = await showTimePicker(context: context, + initialTime: TimeOfDay(hour: date.hour, minute: date.minute), + builder: (BuildContext context, Widget? child) { + Widget w = Theme( + data: ThemeData( + cardTheme: CardTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), + dialogTheme: DialogTheme(elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0))), + colorScheme: ColorScheme.light( + background: Colors.grey.shade300, + tertiary: Colors.grey, + secondary: Colors.grey, + primary: Colors.black), + ), + child: child ?? Container(), + ); + return w; + }, ); - return w; - }, - ); - date = date.add(Duration(hours: time?.hour ?? 0, minutes: time?.minute ?? 0)); + if (time == null) { return null; } + count++; + } + date = date.add(Duration(hours: time?.hour ?? 0, minutes: time?.minute ?? 0)); + widget.item.scheduler["end"] = date.toUtc().toIso8601String(); + widget.item.save!(widget.item.id); } return date; }, - - format: intl.DateFormat('y-M-dd hh:mm:ss'), - initialValue: widget.item.scheduler["end"] != null ? DateTime.parse(widget.item.scheduler["end"]!) : null, - onSaved: (value) { - if (value != null) { - widget.item.scheduler["end"] = "${(value).toIso8601String()}Z"; - } - }, - onChanged: (value) { - if (value == null) { return; } - widget.item.scheduler["end"] = "${value.toIso8601String()}Z"; - }, + format: intl.DateFormat('y-M-dd HH:mm:ss'), + initialValue: end?.toLocal(), style: const TextStyle(fontSize: 12), decoration: InputDecoration( fillColor: Colors.white, @@ -215,7 +273,7 @@ class SchedulerFormsWidgetState extends State { filled: true, alignLabelWithHint: false, hintText: "enter end event...", - labelText: "end event${!(widget.item.schedulerState["service"] == true) ? "*" : ""}", + labelText: "end event${!(widget.item.scheduler["mode"] == 1) ? "*" : ""}", errorStyle: const TextStyle(fontSize: 0), hintStyle: const TextStyle(fontSize: 10), labelStyle: const TextStyle(fontSize: 10), @@ -224,12 +282,18 @@ class SchedulerFormsWidgetState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), ), ))), - widget.item.schedulerState["service"] == true ? Container() : Tooltip( message: "schedule", + widget.item.scheduler["mode"] == 1 ? Container() : Tooltip( message: "schedule", child: Container( height: 40, margin: const EdgeInsets.only(top: 5), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: TextFormField( key: formKeys[3], + enabled: !dash.scheduleActive, initialValue: widget.item.scheduler["cron"], onChanged: (value) { + Future.delayed(const Duration(seconds: 100), () { + if (widget.item.scheduler["cron"] == value) { + widget.item.save!(widget.item.id); + } + }); widget.item.scheduler["cron"] = value; }, onSaved: (value) { @@ -263,9 +327,48 @@ class SchedulerFormsWidgetState extends State { ), ))), const Divider(color: Colors.grey), - Tooltip( message: "save", + Tooltip( message: "check booking", child: InkWell( mouseCursor: SystemMouseCursors.click, onTap: () { + if (dash.scheduler["start"] == null ) { + DateTime now = DateTime.now().add(const Duration(minutes: 5)); + dash.scheduler["start"] = now.toUtc().toIso8601String(); + } + var s = DateTime.parse(dash.scheduler["start"]).toUtc().toIso8601String(); + var e = ""; + if (dash.scheduler["end"] == null) { + e = DateTime.parse(dash.scheduler["start"]).add(const Duration(seconds: 5)).toUtc().toIso8601String(); + } else { + e = DateTime.parse(dash.scheduler["end"]).toUtc().toIso8601String(); + } + check.search(context, [widget.item.id ?? "", s.substring(0, 19), e.substring(0, 19)], {}).then( + (v) { + if (v.data == null) { return; } + widget.booking = v.data!.is_available; + if (v.data!.is_available) { + showAlertBanner( context, () {}, + const InfoAlertBannerChild(text: "no booking found at this date"),// <-- Put any widget here you want! + alertBannerLocation: AlertBannerLocation.bottom,); + } else { + showAlertBanner( context, () {}, + const AlertAlertBannerChild(text: "booking found at this date"),// <-- Put any widget here you want! + alertBannerLocation: AlertBannerLocation.bottom,); + } + setState(() {}); + } + ); + }, child: Container( margin: const EdgeInsets.all(10), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), + border: Border.all(color: widget.booking == null ? Colors.black : (widget.booking == true ? Colors.green : Colors.red), width: 1)), + width: 140, height: 30, + child: Icon( + Icons.verified_outlined, color:widget.booking == null ? Colors.black : (widget.booking == true? Colors.green : Colors.red)), + )) + ), + Tooltip( message: dash.scheduleActive ? "unbook" : "book", + child: InkWell( mouseCursor: SystemMouseCursors.click, + onTap: () { + dash.scheduleActive = !dash.scheduleActive; for (var k in formKeys) { if (k.currentState != null) { if (!k.currentState!.validate()) { @@ -273,12 +376,18 @@ class SchedulerFormsWidgetState extends State { } else { k.currentState!.save();} } } - widget.item.schedulerSave = true; + if (dash.scheduler["start"] == null ) { + DateTime now = DateTime.now().add(const Duration(minutes: 5)); + dash.scheduler["start"] = now.toUtc().toIso8601String(); + } widget.item.save!(widget.item.id); + setState(() { }); }, child: Container( margin: const EdgeInsets.all(10), - decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), border: Border.all(color: Colors.black, width: 1)), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(5), + border: Border.all(color: dash.scheduleActive ? Colors.green : Colors.black, width: 1)), width: 140, height: 30, - child: const Icon(Icons.save_outlined, color: Colors.black), + child: Icon( + dash.scheduleActive ? Icons.cancel_schedule_send : Icons.schedule_send, color: dash.scheduleActive ? Colors.green : Colors.black), )) ), ]); diff --git a/lib/widgets/items/item_row.dart b/lib/widgets/items/item_row.dart index f520ff5..b258bcb 100644 --- a/lib/widgets/items/item_row.dart +++ b/lib/widgets/items/item_row.dart @@ -32,7 +32,8 @@ class ItemRowWidgetState extends State { height: 100, decoration: BoxDecoration( border: Border(bottom: BorderSide(color: Colors.grey.shade300)) ), child: Row( children: [ - widget.low ? Container( padding: const EdgeInsets.only(left: 10),) : Padding( padding: const EdgeInsets.all(10), + widget.low ? Container( padding: const EdgeInsets.only(left: 10),) : Container( padding: const EdgeInsets.all(10), + constraints: BoxConstraints(maxWidth: imageSize, minWidth: imageSize), child: image ?? Image.network('https://get-picto.com/wp-content/uploads/2024/01/logo-instagram-png.webp', height: imageSize, width: imageSize)), Container( diff --git a/lib/widgets/lib/tranformablebox.dart b/lib/widgets/lib/tranformablebox.dart new file mode 100644 index 0000000..bad3572 --- /dev/null +++ b/lib/widgets/lib/tranformablebox.dart @@ -0,0 +1,824 @@ +/* +BSD 3-Clause License + +Copyright (c) 2023, Birju Vachhani +*/ +import 'dart:ui'; + +import 'package:box_transform/box_transform.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_box_transform/flutter_box_transform.dart'; + +/// A widget that allows you to resize and drag a box around a widget. +class TransformableBox extends StatefulWidget { + /// If you need more control over the [TransformableBox] you can pass a + /// custom [TransformableBoxController] instance through the [controller] + /// parameter. + /// + /// If you do not specify one, a default [TransformableBoxController] instance + /// will be created internally, along with its lifecycle. + final TransformableBoxController? controller; + + /// A builder function that is used to build the content of the + /// [TransformableBox]. This is the physical widget you wish to show resizable + /// handles on. It's most commonly something like an image widget, but it + /// could be anything you want to have resizable & draggable box handles on. + final TransformableChildBuilder contentBuilder; + + /// A builder function that is used to build the corners handles of the + /// [TransformableBox]. If you don't specify it, the default handles will be + /// used. + /// + /// Note that this will build for all four corners of the rectangle. + final HandleBuilder cornerHandleBuilder; + + /// A builder function that is used to build the side handles of the + /// [TransformableBox]. If you don't specify it, the default handles will be + /// used. + /// + /// Note that this will build for all four sides of the rectangle. + final HandleBuilder sideHandleBuilder; + + /// The size of the gesture response area of the handles. If you don't + /// specify it, the default value will be used. + /// + /// This is similar to Flutter's [MaterialTapTargetSize] property, in which + /// the actual handle size is smaller than the gesture response area. This is + /// done to improve accessibility and usability of the handles; users will not + /// need cursor precision over the handle's pixels to be able to perform + /// operations with them, they need only to be able to reach the handle's + /// gesture response area to make it forgiving. + /// + /// The default value is 24 pixels in diameter. + final double handleTapSize; + + /// A set containing handles that are enabled. This is different from + /// [visibleHandles]. + /// + /// [enabledHandles] determines which handles are + /// interactive and can be used to resize the box. [visibleHandles] + /// determines which handles are visible. If a handle is visible but not + /// enabled, it will not be interactive. If a handle is enabled but not + /// visible, it will not be shown and will not be interactive. + final Set enabledHandles; + + /// A set containing which handles to show. This is different from + /// [enabledHandles]. + /// + /// [enabledHandles] determines which handles are + /// interactive and can be used to resize the box. [visibleHandles] + /// determines which handles are visible. If a handle is visible but not + /// enabled, it will not be interactive. If a handle is enabled but not + /// visible, it will not be shown and will not be interactive. + final Set visibleHandles; + + /// The initial box that will be used to position set the initial size of + /// the [TransformableBox] widget. + /// + /// This initial box will be mutated by the [TransformableBoxController] through + /// different dragging, panning, and resizing operations. + /// + /// [Rect] is immutable, so a new [Rect] instance will be created every time + /// the [TransformableBoxController] mutates the box. You can acquire your + /// updated box through the [onChanged] callback or through an externally + /// provided [TransformableBoxController] instance. + final Rect rect; + + /// The initial flip that will be used to set the initial flip of the + /// [TransformableBox] widget. Normally, flipping is done by the user through + /// the handles, but you can set the initial flip through this parameter in + /// case the initial state of the box is in a flipped state. + /// + /// This utility cannot predicate if a box is flipped or not, so you will + /// need to provide the correct initial flip state. + /// + /// Note that the flip is optional, if you're resizing an image, for example, + /// you might want to allow flipping of the image when the user drags the + /// handles to opposite corners of the box. This flip behavior is entirely + /// optional and will allow handling such cases. + /// + /// You can leave it at the default [Flip.none] if flipping is not desired. + /// Note that this will not prevent the drag handles from crossing to + /// opposite corners of the box, it will only give oyu a lack of information + /// on the state of the box if flipping were to occur. + final Flip flip; + + /// A box that will contain the [rect] inside of itself, forcing [rect] to + /// be clamped inside of this [clampingRect]. + final Rect clampingRect; + + /// A set of constraints that will be applied to the [rect] when it is + /// resized by the [TransformableBoxController]. + final BoxConstraints constraints; + + /// Whether the box is resizable or not. Setting this to false will disable + /// all resizing operations. This is a convenience parameter that will ignore + /// the [enabledHandles] parameter and set all handles to disabled. + final bool resizable; + + /// Whether the box is movable or not. Setting this to false will disable + /// all moving operations. + final bool draggable; + + /// Whether to allow flipping of the box while resizing. If this is set to + /// true, the box will flip when the user drags the handles to opposite + /// corners of the rect. + final bool allowFlippingWhileResizing; + + /// Decides whether to flip the contents of the box when the box is flipped. + /// If this is set to true, the contents will be flipped when the box is + /// flipped. + final bool allowContentFlipping; + + /// How to align the handles. + final HandleAlignment handleAlignment; + + /// The callback function that is used to resolve the [ResizeMode] based on + /// the pressed keys on the keyboard. + final ValueGetter resizeModeResolver; + + /// A callback that is called every time the [TransformableBox] is updated. + /// This is called every time the [TransformableBoxController] mutates the box + /// or the flip. + final RectChangeEvent? onChanged; + + /// A callback that is called when [TransformableBox] triggers a pointer down + /// event to begin a drag operation. + final RectDragStartEvent? onDragStart; + + /// A callback that is called every time the [TransformableBox] is moved. + /// This is called every time the [TransformableBoxController] mutates the + /// box through a drag operation. + /// + /// This is different from [onChanged] in that it is only called when the + /// box is moved, not when the box is resized. + final RectDragUpdateEvent? onDragUpdate; + + /// A callback that is called every time the [TransformableBox] is completes + /// its drag operation via the pan end event. + final RectDragEndEvent? onDragEnd; + + /// A callback that is called every time the [TransformableBox] cancels + /// its drag operation via the pan cancel event. + final RectDragCancelEvent? onDragCancel; + + /// A callback function that triggers when the box is about to start resizing. + final RectResizeStart? onResizeStart; + + /// A callback that is called every time the [TransformableBox] is resized. + /// This is called every time the [TransformableBoxController] mutates the + /// box. + /// + /// This is different from [onChanged] in that it is only called when the box + /// is resized, not when the box is moved. + final RectResizeUpdateEvent? onResizeUpdate; + + /// A callback function that triggers when the box is about to end resizing. + final RectResizeEnd? onResizeEnd; + + /// A callback function that triggers when the box cancels resizing. + final RectResizeCancel? onResizeCancel; + + /// A callback function that triggers when the box reaches its minimum width + /// when resizing. + final TerminalEdgeEvent? onMinWidthReached; + + /// A callback function that triggers when the box reaches its maximum width + /// when resizing. + final TerminalEdgeEvent? onMaxWidthReached; + + /// A callback function that triggers when the box reaches its minimum height + /// when resizing. + final TerminalEdgeEvent? onMinHeightReached; + + /// A callback function that triggers when the box reaches its maximum height + /// when resizing. + final TerminalEdgeEvent? onMaxHeightReached; + + /// A callback function that triggers when the box reaches a terminal width + /// when resizing. A terminal width is a width that is either the minimum or + /// maximum width of the box. + /// + /// This function combines both [onMinWidthReached] and [onMaxWidthReached] + /// into one callback function. + final TerminalAxisEvent? onTerminalWidthReached; + + /// A callback function that triggers when the box reaches a terminal height + /// when resizing. A terminal height is a height that is either the minimum or + /// maximum height of the box. + /// + /// This function combines both [onMinHeightReached] and [onMaxHeightReached] + /// into one callback function. + final TerminalAxisEvent? onTerminalHeightReached; + + /// A callback function that triggers when the box reaches a terminal size + /// when resizing. A terminal size is a size that is either the minimum or + /// maximum size of the box on either axis. + /// + /// This function combines both [onTerminalWidthReached] and + /// [onTerminalHeightReached] into one callback function. + final TerminalEvent? onTerminalSizeReached; + + /// Whether to paint the handle's bounds for debugging purposes. + final bool debugPaintHandleBounds; + final double handleTapLeftSize; + /// Creates a [TransformableBox] widget. + const TransformableBox({ + super.key, + required this.contentBuilder, + this.controller, + this.cornerHandleBuilder = _defaultCornerHandleBuilder, + this.sideHandleBuilder = _defaultSideHandleBuilder, + this.handleTapSize = 24, + this.handleTapLeftSize = 24, + this.allowContentFlipping = true, + this.handleAlignment = HandleAlignment.center, + this.enabledHandles = const {...HandlePosition.values}, + this.visibleHandles = const {...HandlePosition.values}, + + // Raw values. + Rect? rect, + Flip? flip, + Rect? clampingRect, + BoxConstraints? constraints, + ValueGetter? resizeModeResolver, + + // Additional controls. + this.resizable = true, + this.draggable = true, + this.allowFlippingWhileResizing = true, + + // Either resize or drag triggers. + this.onChanged, + + // Resize events + this.onResizeStart, + this.onResizeUpdate, + this.onResizeEnd, + this.onResizeCancel, + + // Drag Events. + this.onDragStart, + this.onDragUpdate, + this.onDragEnd, + this.onDragCancel, + + // Terminal update events. + this.onMinWidthReached, + this.onMaxWidthReached, + this.onMinHeightReached, + this.onMaxHeightReached, + this.onTerminalWidthReached, + this.onTerminalHeightReached, + this.onTerminalSizeReached, + this.debugPaintHandleBounds = false, + }) : assert( + (controller == null) || + ((rect == null) && + (flip == null) && + (clampingRect == null) && + (constraints == null) && + (resizeModeResolver == null)), + 'If a controller is provided, the raw values should not be provided.', + ), + rect = rect ?? Rect.zero, + flip = flip ?? Flip.none, + clampingRect = clampingRect ?? Rect.largest, + constraints = constraints ?? const BoxConstraints.expand(), + resizeModeResolver = resizeModeResolver ?? defaultResizeModeResolver; + + /// Returns the [TransformableBox] of the closest ancestor. + static TransformableBox? widgetOf(BuildContext context) { + return context.findAncestorWidgetOfExactType(); + } + + /// Returns the [TransformableBoxController] of the closest ancestor. + static TransformableBoxController? controllerOf(BuildContext context) { + return context + .findAncestorStateOfType<_TransformableBoxState>() + ?.controller; + } + + @override + State createState() => _TransformableBoxState(); +} + +class _TransformableBoxState extends State { + late TransformableBoxController controller; + + bool isLegalGesture = false; + + @override + void initState() { + super.initState(); + + if (widget.controller != null) { + controller = widget.controller!; + // We only want to listen to the controller if it is provided externally. + controller.addListener(onControllerUpdate); + } else { + // If it is provided internally, we should not listen to it. + controller = TransformableBoxController( + rect: widget.rect, + flip: widget.flip, + clampingRect: widget.clampingRect, + constraints: widget.constraints, + resizeModeResolver: widget.resizeModeResolver, + allowFlippingWhileResizing: widget.allowFlippingWhileResizing, + ); + } + } + + @override + void didUpdateWidget(covariant TransformableBox oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.controller != null && oldWidget.controller == null || + widget.controller != oldWidget.controller) { + // New explicit controller provided or explicit controller changed. + controller.removeListener(onControllerUpdate); + controller = widget.controller!; + controller.addListener(onControllerUpdate); + } else if (oldWidget.controller != null && widget.controller == null) { + // Explicit controller removed. + controller.removeListener(onControllerUpdate); + controller = TransformableBoxController( + rect: widget.rect, + flip: widget.flip, + clampingRect: widget.clampingRect, + constraints: widget.constraints, + resizeModeResolver: widget.resizeModeResolver, + allowFlippingWhileResizing: widget.allowFlippingWhileResizing, + ); + } + + // Return if the controller is external. + if (widget.controller != null) return; + + // Below code should only be executed if the controller is internal. + bool shouldRecalculatePosition = false; + bool shouldRecalculateSize = false; + + if (oldWidget.rect != widget.rect) { + controller.setRect(widget.rect, notify: false); + } + + if (oldWidget.flip != widget.flip) { + controller.setFlip(widget.flip, notify: false); + } + + if (oldWidget.resizeModeResolver != widget.resizeModeResolver) { + controller.setResizeModeResolver( + widget.resizeModeResolver, + notify: false, + ); + } + + if (oldWidget.clampingRect != widget.clampingRect) { + controller.setClampingRect(widget.clampingRect, notify: false); + shouldRecalculatePosition = true; + } + + if (oldWidget.constraints != widget.constraints) { + controller.setConstraints(widget.constraints, notify: false); + shouldRecalculateSize = true; + } + + if (oldWidget.allowFlippingWhileResizing != + widget.allowFlippingWhileResizing) { + controller.setAllowFlippingWhileResizing( + widget.allowFlippingWhileResizing, + notify: false, + ); + } + + if (shouldRecalculatePosition) { + controller.recalculatePosition(notify: false); + } + + if (shouldRecalculateSize) { + controller.recalculateSize(notify: false); + } + } + + @override + void dispose() { + controller.removeListener(onControllerUpdate); + if (widget.controller == null) controller.dispose(); + super.dispose(); + } + + /// Called when the controller is updated. + void onControllerUpdate() { + if (widget.rect != controller.rect || widget.flip != controller.flip) { + if (mounted) setState(() {}); + } + } + + /// Called when the handle drag starts. + void onHandlePanStart(DragStartDetails event, HandlePosition handle) { + // Two fingers were used to start the drag. This produces issues with + // the box drag event. Therefore, we ignore it. + if (event.kind == PointerDeviceKind.trackpad) { + isLegalGesture = false; + return; + } else { + isLegalGesture = true; + } + + controller.onResizeStart(event.localPosition); + widget.onResizeStart?.call(handle, event); + } + + /// Called when the handle drag updates. + void onHandlePanUpdate(DragUpdateDetails event, HandlePosition handle) { + if (!isLegalGesture) return; + + final UIResizeResult result = controller.onResizeUpdate( + event.localPosition, + handle, + ); + + widget.onChanged?.call(result, event); + widget.onResizeUpdate?.call(result, event); + widget.onMinWidthReached?.call(result.minWidthReached); + widget.onMaxWidthReached?.call(result.maxWidthReached); + widget.onMinHeightReached?.call(result.minHeightReached); + widget.onMaxHeightReached?.call(result.maxHeightReached); + widget.onTerminalWidthReached?.call( + result.minWidthReached, + result.maxWidthReached, + ); + widget.onTerminalHeightReached?.call( + result.minHeightReached, + result.maxHeightReached, + ); + widget.onTerminalSizeReached?.call( + result.minWidthReached, + result.maxWidthReached, + result.minHeightReached, + result.maxHeightReached, + ); + } + + /// Called when the handle drag ends. + void onHandlePanEnd(DragEndDetails event, HandlePosition handle) { + if (!isLegalGesture) return; + + controller.onResizeEnd(); + widget.onResizeEnd?.call(handle, event); + widget.onMinWidthReached?.call(false); + widget.onMaxWidthReached?.call(false); + widget.onMinHeightReached?.call(false); + widget.onMaxHeightReached?.call(false); + widget.onTerminalWidthReached?.call(false, false); + widget.onTerminalHeightReached?.call(false, false); + widget.onTerminalSizeReached?.call(false, false, false, false); + } + + void onHandlePanCancel(HandlePosition handle) { + if (!isLegalGesture) return; + + controller.onResizeEnd(); + widget.onResizeCancel?.call(handle); + widget.onMinWidthReached?.call(false); + widget.onMaxWidthReached?.call(false); + widget.onMinHeightReached?.call(false); + widget.onMaxHeightReached?.call(false); + widget.onTerminalWidthReached?.call(false, false); + widget.onTerminalHeightReached?.call(false, false); + widget.onTerminalSizeReached?.call(false, false, false, false); + } + + /// Called when the box drag event starts. + void onDragPanStart(DragStartDetails event) { + // Two fingers were used to start the drag. This produces issues with + // the box drag event. Therefore, we ignore it. + if (event.kind == PointerDeviceKind.trackpad) { + isLegalGesture = false; + return; + } else { + isLegalGesture = true; + } + + controller.onDragStart(event.localPosition); + widget.onDragStart?.call(event); + } + + /// Called when the box drag event updates. + void onDragPanUpdate(DragUpdateDetails event) { + if (!isLegalGesture) return; + + final UIMoveResult result = controller.onDragUpdate( + event.localPosition, + ); + + widget.onChanged?.call(result, event); + widget.onDragUpdate?.call(result, event); + } + + /// Called when the box drag event ends. + void onDragPanEnd(DragEndDetails event) { + if (!isLegalGesture) return; + + controller.onDragEnd(); + widget.onDragEnd?.call(event); + } + + void onDragPanCancel() { + if (!isLegalGesture) return; + + controller.onDragEnd(); + widget.onDragCancel?.call(); + } + + @override + Widget build(BuildContext context) { + final Flip flip = controller.flip; + final Rect rect = controller.rect; + + Widget content = Transform.scale( + scaleX: widget.allowContentFlipping && flip.isHorizontal ? -1 : 1, + scaleY: widget.allowContentFlipping && flip.isVertical ? -1 : 1, + child: widget.contentBuilder(context, rect, flip), + ); + + if (widget.draggable) { + content = GestureDetector( + behavior: HitTestBehavior.translucent, + onPanStart: onDragPanStart, + onPanUpdate: onDragPanUpdate, + onPanEnd: onDragPanEnd, + onPanCancel: onDragPanCancel, + child: content, + ); + } + + return SizedBox( + width: rect.width, + height: rect.height, + child: Stack( + clipBehavior: Clip.none, + fit: StackFit.expand, + children: [ + Positioned( + left: widget.handleAlignment.offset(widget.handleTapLeftSize), + top: widget.handleAlignment.offset(widget.handleTapSize), + width: rect.width, + height: rect.height, + child: content, + ), + if (widget.resizable) + for (final handle in HandlePosition.corners.where((handle) => + widget.visibleHandles.contains(handle) || + widget.enabledHandles.contains(handle))) + CornerHandleWidget( + key: ValueKey(handle), + handlePosition: handle, + handleTapSize: widget.handleTapSize, + enabled: widget.enabledHandles.contains(handle), + visible: widget.visibleHandles.contains(handle), + onPanStart: (event) => onHandlePanStart(event, handle), + onPanUpdate: (event) => onHandlePanUpdate(event, handle), + onPanEnd: (event) => onHandlePanEnd(event, handle), + onPanCancel: () => onHandlePanCancel(handle), + builder: widget.cornerHandleBuilder, + ), + if (widget.resizable) + for (final handle in HandlePosition.sides.where((handle) => + widget.visibleHandles.contains(handle) || + widget.enabledHandles.contains(handle))) + SideHandleWidget( + key: ValueKey(handle), + handlePosition: handle, + handleTapSize: widget.handleTapSize, + enabled: widget.enabledHandles.contains(handle), + visible: widget.visibleHandles.contains(handle), + onPanStart: (event) => onHandlePanStart(event, handle), + onPanUpdate: (event) => onHandlePanUpdate(event, handle), + onPanEnd: (event) => onHandlePanEnd(event, handle), + onPanCancel: () => onHandlePanCancel(handle), + builder: widget.sideHandleBuilder, + ), + ], + ) , + ); + } +} + +/// A default implementation of the corner [HandleBuilder] callback. +Widget _defaultCornerHandleBuilder( + BuildContext context, + HandlePosition handle, +) => + DefaultCornerHandle(handle: handle); + +/// A default implementation of the side [HandleBuilder] callback. +Widget _defaultSideHandleBuilder( + BuildContext context, + HandlePosition handle, +) => + DefaultSideHandle(handle: handle); + + +@protected +class CornerHandleWidget extends StatelessWidget { + /// The position of the handle. + final HandlePosition handlePosition; + + /// The builder that is used to build the handle widget. + final HandleBuilder builder; + + /// The size of the handle's gesture response area. + final double handleTapSize; + + /// Called when the handle dragging starts. + final GestureDragStartCallback? onPanStart; + + /// Called when the handle dragging is updated. + final GestureDragUpdateCallback? onPanUpdate; + + /// Called when the handle dragging ends. + final GestureDragEndCallback? onPanEnd; + + /// Called when the handle dragging is canceled. + final GestureDragCancelCallback? onPanCancel; + + /// Whether the handle is resizable. + final bool enabled; + + /// Whether the handle is visible. + final bool visible; + + /// Whether to paint the handle's bounds for debugging purposes. + final bool debugPaintHandleBounds; + + /// Creates a new handle widget. + CornerHandleWidget({ + super.key, + required this.handlePosition, + required this.handleTapSize, + required this.builder, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd, + this.onPanCancel, + this.enabled = true, + this.visible = true, + this.debugPaintHandleBounds = false, + }) : assert(handlePosition.isDiagonal, 'A corner handle must be diagonal.'); + + @override + Widget build(BuildContext context) { + Widget child = + visible ? builder(context, handlePosition) : const SizedBox.shrink(); + + if (enabled) { + child = GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: onPanStart, + onPanUpdate: onPanUpdate, + onPanEnd: onPanEnd, + onPanCancel: onPanCancel, + child: MouseRegion( + cursor: getCursorForHandle(handlePosition), + child: child, + ), + ); + } + + return Positioned( + left: handlePosition.influencesLeft ? 0 : null, + right: handlePosition.influencesRight ? 0 : null, + top: handlePosition.influencesTop ? 0 : null, + bottom: handlePosition.influencesBottom ? 0 : null, + width: handleTapSize, + height: handleTapSize, + child: child, + ); + } + + /// Returns the cursor for the given handle position. + MouseCursor getCursorForHandle(HandlePosition handle) { + switch (handle) { + case HandlePosition.topLeft: + case HandlePosition.bottomRight: + return SystemMouseCursors.resizeUpLeftDownRight; + case HandlePosition.topRight: + case HandlePosition.bottomLeft: + return SystemMouseCursors.resizeUpRightDownLeft; + default: + throw Exception('Invalid handle position.'); + } + } +} + +/// Creates a new cardinal handle widget, with its appropriate gesture splash +/// zone. +@protected +class SideHandleWidget extends StatelessWidget { + /// The position of the handle. + final HandlePosition handlePosition; + + /// The builder that is used to build the handle widget. + final HandleBuilder builder; + + /// The thickness of the handle that is used for gesture detection. + final double handleTapSize; + + /// Called when the handle dragging starts. + final GestureDragStartCallback? onPanStart; + + /// Called when the handle dragging is updated. + final GestureDragUpdateCallback? onPanUpdate; + + /// Called when the handle dragging ends. + final GestureDragEndCallback? onPanEnd; + + /// Called when the handle dragging is canceled. + final GestureDragCancelCallback? onPanCancel; + + /// Whether the handle is resizable. + final bool enabled; + + /// Whether the handle is visible. + final bool visible; + + /// Whether to paint the handle's bounds for debugging purposes. + final bool debugPaintHandleBounds; + + /// Creates a new handle widget. + SideHandleWidget({ + super.key, + required this.handlePosition, + required this.handleTapSize, + required this.builder, + this.onPanStart, + this.onPanUpdate, + this.onPanEnd, + this.onPanCancel, + this.enabled = true, + this.visible = true, + this.debugPaintHandleBounds = false, + }) : assert(handlePosition.isSide, 'A cardinal handle must be cardinal.'); + + @override + Widget build(BuildContext context) { + Widget child = + visible ? builder(context, handlePosition) : const SizedBox.shrink(); + + if (enabled) { + child = GestureDetector( + behavior: HitTestBehavior.opaque, + onPanStart: onPanStart, + onPanUpdate: onPanUpdate, + onPanEnd: onPanEnd, + onPanCancel: onPanCancel, + child: MouseRegion( + cursor: getCursorForHandle(handlePosition), + child: child, + ), + ); + } + + return Positioned( + left: handlePosition.isVertical + ? handleTapSize + : handlePosition.influencesLeft + ? 0 + : null, + right: handlePosition.isVertical + ? handleTapSize + : handlePosition.influencesRight + ? 0 + : null, + top: handlePosition.isHorizontal + ? handleTapSize + : handlePosition.influencesTop + ? 0 + : null, + bottom: handlePosition.isHorizontal + ? handleTapSize + : handlePosition.influencesBottom + ? 0 + : null, + width: handlePosition.isHorizontal ? handleTapSize : null, + height: handlePosition.isVertical ? handleTapSize : null, + child: child, + ); + } + + /// Returns the cursor for the given handle position. + MouseCursor getCursorForHandle(HandlePosition handle) { + switch (handle) { + case HandlePosition.left: + case HandlePosition.right: + return SystemMouseCursors.resizeLeftRight; + case HandlePosition.top: + case HandlePosition.bottom: + return SystemMouseCursors.resizeUpDown; + default: + throw Exception('Invalid handle position.'); + } + } +} diff --git a/lib/widgets/logs.dart b/lib/widgets/logs.dart index 9e151da..0a450aa 100644 --- a/lib/widgets/logs.dart +++ b/lib/widgets/logs.dart @@ -1,18 +1,28 @@ import 'package:alert_banner/exports.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:json_string/json_string.dart'; +import 'package:oc_front/core/sections/header/header.dart'; import 'package:oc_front/models/logs.dart'; import 'package:oc_front/widgets/dialog/alert.dart'; class LogsWidget extends StatefulWidget { final List items; - LogsWidget ({ Key? key, required this.items }): super(key: key); + String? level; + String search = ""; + LogsWidget ({ Key? key, this.search = "", required this.items, this.level }): super(key: key); @override LogsWidgetState createState() => LogsWidgetState(); } class LogsWidgetState extends State { @override Widget build(BuildContext context) { - List itemRows = widget.items.map((e) => LogWidget(item: e)).toList(); - return SingleChildScrollView( child: Column( children: itemRows ) ); + List itemRows = widget.items.where((element) => (element.message?.toLowerCase().contains(widget.search.toLowerCase()) ?? true) + && (widget.level?.contains(element.level ?? "") ?? true) ).map((e) => LogWidget(item: e)).toList(); + return Stack( children: [ + SingleChildScrollView( child: itemRows.isEmpty ? + Container( height: MediaQuery.of(context).size.height - 100 - HeaderConstants.height, + child: const Center( child: Text("no log registered", style: TextStyle(color: Colors.grey, fontSize: 25 ),))) + : Column( children: [...itemRows, Container(height: 50,) ] ) ), + ]); } } @@ -24,23 +34,31 @@ class LogWidget extends StatefulWidget { } class LogWidgetState extends State { @override Widget build(BuildContext context) { - return Padding( padding: const EdgeInsets.only(top: 10, left: 30, right: 30), child: Wrap( children: [ + Map map = {}; + try { map = JsonString(widget.item.rawMessage?.replaceAll("\\", "") ?? "").decodedValue as Map; + } catch (e) { /* */} + return Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white)), + ), + padding: const EdgeInsets.only(top: 10, left: 30, right: 30, bottom: 10), + child: Wrap( children: [ Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 10, height: 15, color: widget.item.level?.toLowerCase() == "info" ? Colors.green : ( widget.item.level?.toLowerCase() == "error" ? Colors.red : ( widget.item.level?.toLowerCase() == "warning" ? Colors.orange : Colors.blue))), - InkWell( mouseCursor: widget.item.map.isEmpty ? MouseCursor.defer : SystemMouseCursors.click, onTap: () { - if (widget.item.map.isNotEmpty ) { + InkWell( mouseCursor: map.isEmpty ? MouseCursor.defer : SystemMouseCursors.click, onTap: () { + if (map.isNotEmpty ) { setState(() { widget.expanded = !widget.expanded; }); } - }, child: Container( height: 20, + }, child: SizedBox( height: 20, child: Padding( padding: EdgeInsets.symmetric(horizontal: widget.expanded ? 0 : 5), child: Icon( widget.expanded ? Icons.keyboard_arrow_down_outlined : Icons.arrow_forward_ios, size: widget.expanded ? 25 : 15, - color: widget.item.map.isEmpty ? Colors.grey : Colors.black, weight: widget.expanded ? 100 : 1000,)))), + color: map.isEmpty ? Colors.grey : Colors.black, weight: widget.expanded ? 100 : 1000,)))), Padding( padding: const EdgeInsets.only(right: 10), child: Text("${widget.item.timestamp?.toString()}", style: const TextStyle(fontSize: 13, color: Colors.black, fontWeight: FontWeight.w500))), @@ -57,10 +75,10 @@ class LogWidgetState extends State { decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(4)), padding: const EdgeInsets.all(10), - child: Column( children: widget.item.map.keys.map((e) => + child: Column( children: map.keys.map((e) => Padding( padding: const EdgeInsets.all(2), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - children: [Flexible( child:Text("$e: \"${widget.item.map[e]}\"", + children: [Flexible( child:Text("$e: \"${map[e]}\"", style: const TextStyle(fontSize: 11, color: Colors.white))), ]) )).toList() )) : Container(), diff --git a/lib/widgets/menu_clipper/workspace_menu.dart b/lib/widgets/menu_clipper/workspace_menu.dart index 10545e6..c5fc1af 100644 --- a/lib/widgets/menu_clipper/workspace_menu.dart +++ b/lib/widgets/menu_clipper/workspace_menu.dart @@ -1,7 +1,3 @@ - - -import 'dart:ffi'; - import 'package:flutter/material.dart'; import 'package:oc_front/core/models/workspace_local.dart'; class MenuWorkspaceWidget extends StatefulWidget { diff --git a/lib/widgets/sheduler_items/schedule.dart b/lib/widgets/sheduler_items/schedule.dart index 9ebf4f9..242bdd1 100644 --- a/lib/widgets/sheduler_items/schedule.dart +++ b/lib/widgets/sheduler_items/schedule.dart @@ -1,26 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_box_transform/flutter_box_transform.dart'; import 'package:oc_front/core/sections/header/header.dart'; import 'package:oc_front/core/services/specialized_services/logs_service.dart'; import 'package:oc_front/models/logs.dart'; import 'package:oc_front/models/workflow.dart'; import 'package:oc_front/widgets/logs.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:oc_front/widgets/lib/tranformablebox.dart' as fork; import 'package:oc_front/widgets/sheduler_items/scheduler_calendar.dart'; import 'package:oc_front/widgets/sheduler_items/scheduler_item.dart'; +double menuSize = 300; // ignore: must_be_immutable class ScheduleWidget extends StatefulWidget { DateTime start; DateTime end; bool isDayPlanner = true; + bool loading = true; Map> data; bool isList = true; - ScheduleWidget ({ super.key, required this.data, required this.start, required this.end, this.isList = true }); + ScheduleWidget ({ super.key, required this.data, required this.start, required this.end, this.isList = true, this.loading = false}); @override ScheduleWidgetState createState() => ScheduleWidgetState(); } +String? selected; +String? selectedReal; class ScheduleWidgetState extends State { LogsService _service = LogsService(); - String? selected; - String? selectedReal; + String search = ""; + String? level; List colors = [Colors.blue, Colors.orange, Colors.red, Colors.green]; List titles = ["SCHEDULED", "RUNNING", "FAILURE", "SUCCESS"]; @@ -31,7 +39,7 @@ class ScheduleWidgetState extends State { @override Widget build(BuildContext context) { bool isInfo = MediaQuery.of(context).size.width <= 600 && selected != null; - double w = selected != null ? MediaQuery.of(context).size.width - 300 : MediaQuery.of(context).size.width; + double w = selected != null ? MediaQuery.of(context).size.width - menuSize : MediaQuery.of(context).size.width; List children = []; if (selected != null) { for (var wf in widget.data[selected!] ?? ([])) { @@ -40,7 +48,8 @@ class ScheduleWidgetState extends State { onTap: () => setState(() { selectedReal = wf.executionData; }), child: Container( margin: const EdgeInsets.all(10), decoration: BoxDecoration( - border: Border.all(color: selectedReal != null && selectedReal == wf.executionData ? const Color.fromRGBO(38, 166, 154, 1) : Colors.transparent, width: 2), + border: Border.all(color: selectedReal != null && selectedReal == wf.executionData ? + const Color.fromRGBO(38, 166, 154, 1) : Colors.transparent, width: 2), borderRadius: BorderRadius.circular(4), color: Colors.white ), child: Container( @@ -52,13 +61,13 @@ class ScheduleWidgetState extends State { borderRadius: BorderRadius.circular(4), ), ), - Container( width: (400 - 250), + SizedBox( width: (menuSize - 140), child: Padding( padding: const EdgeInsets.only(left: 20), child: Text(wf.name?.toUpperCase() ?? "", overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.black, fontSize: 12, fontWeight: FontWeight.w500)), )), - Container( + SizedBox( child: Padding( padding: const EdgeInsets.only(left: 20), child: Text("${d2.hour > 9 ? d2.hour : "0${d2.hour}"}:${d2.minute > 9 ? d2.minute : "0${d2.minute}"}", overflow: TextOverflow.ellipsis, @@ -70,48 +79,45 @@ class ScheduleWidgetState extends State { } } + List logs = []; String? selectedID; - String? start; - String? end; if (selectedReal != null) { try { var sel = widget.data[selected!]!.firstWhere((element) => element.executionData == selectedReal); selectedID = sel.id; - print(sel.endDate); - if (sel.endDate != null && sel.endDate != "") { - var startD = DateTime.parse(sel.executionData!); - var endD = DateTime.parse(sel.endDate!); - var diff = endD.difference(startD); - if (diff.inDays < 30) { - var rest = ((30 - diff.inDays) ~/ 2) - 1; - start = (startD.subtract(Duration(days: rest)).microsecondsSinceEpoch).toString(); - end = (endD.add(Duration(days: rest)).microsecondsSinceEpoch).toString(); - } else { - start = (startD.microsecondsSinceEpoch).toString(); - end = (startD.add( const Duration(days: 29)).microsecondsSinceEpoch).toString(); - } - } else { - start = (DateTime.parse(sel.executionData!).subtract( const Duration(days: 14)).microsecondsSinceEpoch).toString(); - end = (DateTime.parse(sel.executionData!).add( const Duration(days: 14)).microsecondsSinceEpoch).toString(); - } + logs = sel.logs ?? []; } catch(e) { /* */ } } + menuSize = isInfo ? MediaQuery.of(context).size.width : (menuSize > MediaQuery.of(context).size.width / 2 ? MediaQuery.of(context).size.width / 2 : menuSize); + Rect rect = Rect.fromCenter( center: MediaQuery.of(context).size.center(Offset.zero), + width: selected != null ? menuSize : 0, height: (MediaQuery.of(context).size.height - HeaderConstants.height - 50) > 0 ? (MediaQuery.of(context).size.height - HeaderConstants.height - 50) : 0); return Row( children: [ isInfo ? Container() : SizedBox( width: w, child: widget.isList ? SchedulerItemWidget(data: widget.data, parent: this, focusedDay: getFocusedDay(), width: w) : SchedulerCalendarWidget(data: widget.data, start: widget.start, end: widget.end, parent: this, focusedDay: getFocusedDay(),) ), - Container( + fork.TransformableBox( + rect: rect, constraints: BoxConstraints( + maxWidth: isInfo ? MediaQuery.of(context).size.width : (selected != null ? MediaQuery.of(context).size.width / 2 : 0), + minWidth: selected != null ? 300 : 0), + handleTapSize: 1, handleTapLeftSize: 0, allowFlippingWhileResizing: false, draggable: false, flip: null, + resizeModeResolver: () => ResizeMode.freeform, + visibleHandles: const {HandlePosition.left}, + enabledHandles: const {HandlePosition.left}, + clampingRect: Offset.zero & MediaQuery.sizeOf(context), + handleAlignment: HandleAlignment.inside, + onChanged: (result, event) { setState(() { menuSize = result.rect.width; }); }, + contentBuilder: (context, rect, flip) { return Container( height: MediaQuery.of(context).size.height - HeaderConstants.height - 50, - width: isInfo ? MediaQuery.of(context).size.width : (selected != null ? 300 : 0), + width: isInfo ? MediaQuery.of(context).size.width : (selected != null ? menuSize : 0), color: Colors.grey.shade300, child: Column( children: [ Row( children: [ InkWell( onTap: () => setState(() { widget.isDayPlanner = true; }), child: Tooltip( message: "day planning", child: - Container( height: 50, width: (isInfo ? MediaQuery.of(context).size.width : (selected != null ? 300 : 0)) / (selectedReal != null ? 2 : 1 ), + Container( height: 50, width: (isInfo ? MediaQuery.of(context).size.width : (selected != null ? menuSize : 0)) / (selectedReal != null ? 2 : 1 ), alignment: Alignment.center, decoration: BoxDecoration( color: widget.isDayPlanner ? Colors.grey : Colors.transparent, @@ -121,7 +127,8 @@ class ScheduleWidgetState extends State { )), InkWell( onTap: () => setState(() { widget.isDayPlanner = false; }), child: Tooltip( message: "monitor task", child: - Container( height: 50, width: selectedReal == null ? 0 : ((isInfo ? MediaQuery.of(context).size.width : (selected != null ? 300 : 0)) / 2), + Container( height: 50, width: selectedReal == null ? 0 : ( + (isInfo ? MediaQuery.of(context).size.width : (selected != null ? menuSize : 0)) / 2), alignment: Alignment.center, decoration: BoxDecoration( color: !widget.isDayPlanner ? Colors.grey : Colors.transparent, @@ -132,48 +139,84 @@ class ScheduleWidgetState extends State { ) )) ]), - Container( width: isInfo ? MediaQuery.of(context).size.width : (selected != null ? 300 : 0), height: MediaQuery.of(context).size.height - HeaderConstants.height - 100, - child: SingleChildScrollView( child: Column( - mainAxisAlignment: children.isEmpty ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - ...( widget.isDayPlanner ? children : ( selectedID != null ? [ - FutureBuilder(future: _service.search(context, [], { - "workflow_execution_id": selectedID, - "start": start, - "end": end - }), builder: (ctx, as) { - var speLog = Log(level: "error", timestamp: DateTime.now()); - speLog.getMessage("{\"Name\":\"oc-monitor-unonip-fauta9hswg\",\"Namespace\":\"argo\",\"Status\":\"Pending\",\"PodRunning\":false,\"Completed\":false,\"Created\":\"Tue Aug 06 11:33:52 +0200 (now)\",\"Started\":\"\",\"Duration\":\"\",\"Progress\":\"\"}"); - var speLog2 = Log(level: "warning", timestamp: DateTime.now()); - speLog2.getMessage("{\"Name\":\"oc-monitor-unonip-fauta9hswg\",\"Namespace\":\"argo\",\"Status\":\"Running\",\"PodRunning\":false,\"Completed\":false,\"Created\":\"Tue Aug 06 11:33:52 +0200 (now)\",\"Started\":\"Tue Aug 06 11:33:52 +0200 (now)\",\"Duration\":\"0 seconds\",\"Progress\":\"0/1\"}"); - List logs = [ - Log( - level: "info", - message: "No logs found", - timestamp: DateTime.now() + SizedBox( width: isInfo ? MediaQuery.of(context).size.width : (selected != null ? menuSize : 0), height: MediaQuery.of(context).size.height - HeaderConstants.height - (!widget.isDayPlanner && !widget.loading ? 150 : 100 ), + child: Stack( children: [ + SingleChildScrollView( child: Column( + mainAxisAlignment: children.isEmpty || widget.loading ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + ...( widget.isDayPlanner ? children : ( selectedID != null ? [ + widget.loading ? const SpinKitCircle(color: Colors.white,) : LogsWidget(items: logs, search: search, level: level) + ] : [])), + children.isEmpty ? Container( height: 100, alignment: Alignment.center, child: const Text("No event found", style: TextStyle(color: Colors.grey, fontSize: 20))) : Container() + ]), + ) ]) + ), + !widget.isDayPlanner && !widget.loading ? + Row( children: [ + Container( + width: 150, + height: 50, + decoration: BoxDecoration( + border: Border(left: BorderSide(color: Colors.grey.shade300)), + ), + child: DropdownButtonFormField( + isExpanded: true, + value: level, + style: const TextStyle(fontSize: 12), + hint: const Text("by level...", style: TextStyle(fontSize: 12)), + decoration: InputDecoration( + filled: true, + focusedBorder: const OutlineInputBorder( borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Color.fromARGB(38, 166, 154, 1), width: 0), + ), + fillColor: Colors.white, + contentPadding: const EdgeInsets.only(left: 30, right: 30, top: 10, bottom: 30), + enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Colors.grey.shade300, width: 0), + ), + border: OutlineInputBorder( borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Colors.grey.shade300, width: 0)), ), - speLog, - speLog2 - ]; - if (as.hasData && as.data!.data != null) { - var d = as.data!.data!; - for( var r in d.data?.result ?? []) { - for (var element in r.logs) { - element.level = r.level; - logs.add(element); - } - } - } - logs.sort((a, b) => a.timestamp!.compareTo(b.timestamp!)); - return LogsWidget(items: logs); - }) - ] : [])), - children.isEmpty ? Container( height: 100, alignment: Alignment.center, child: const Text("No event found", style: const TextStyle(color: Colors.grey, fontSize: 20))) : Container() - ])) - ) + items: [ + DropdownMenuItem(value: "debug,warning,error,info", child: Row( children: [ Container( width: 10, height: 15, color: Colors.grey), Text(" all", style: TextStyle(fontSize: 12, color: Colors.black)) ])), + DropdownMenuItem(value: "debug", child: Row( children: [ Container( width: 10, height: 15, color: Colors.blue), Text(" debug", style: TextStyle(fontSize: 12, color: Colors.black)) ])), + DropdownMenuItem(value: "warning", child: Row( children: [ Container( width: 10, height: 15, color: Colors.orange), Text(" warning", style: TextStyle(fontSize: 12, color: Colors.black)) ])), + DropdownMenuItem(value: "error", child: Row( children: [ Container( width: 10, height: 15, color: Colors.red), Text(" error", style: TextStyle(fontSize: 12, color: Colors.black)) ])), + DropdownMenuItem(value: "info", child: Row( children: [ Container( width: 10, height: 15, color: Colors.green), Text(" info", style: TextStyle(fontSize: 12, color: Colors.black)) ])), + ], + onChanged: (value) { + setState(() { + level = value; + }); + })), + Container( + width: menuSize - 150, + height: 50, + decoration: BoxDecoration( + border: Border(left: BorderSide(color: Colors.grey.shade300)), + ), + child: TextField( + onChanged: (value) { setState(() { + search = value; + });}, + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration( + hintText: "by logs...", + fillColor: Colors.white, + filled: true, + contentPadding: EdgeInsets.symmetric(horizontal: 30), + hintStyle: TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w300 + ), + border: InputBorder.none + ) + )) ] + ) : Container( ), ], ), - ) + ); }) ]); } } \ No newline at end of file diff --git a/lib/widgets/sheduler_items/scheduler_calendar.dart b/lib/widgets/sheduler_items/scheduler_calendar.dart index 6fd4e30..3a3367c 100644 --- a/lib/widgets/sheduler_items/scheduler_calendar.dart +++ b/lib/widgets/sheduler_items/scheduler_calendar.dart @@ -50,13 +50,14 @@ class SchedulerCalendarWidgetState extends State { markerBuilder: (context, day, events) { List children = []; for (var ev in events) { - if (children.length == 2 && events.length > 3) { + if (children.length == 1 && events.length > 2) { children.add( InkWell( onTap: () => widget.parent!.setState(() { - widget.parent!.selected = day.toIso8601String(); - widget.parent!.selectedReal = null; + selected = day.toIso8601String(); + selectedReal = null; widget.parent!.widget.isDayPlanner = true; }), child: Container( + margin: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), @@ -67,13 +68,14 @@ class SchedulerCalendarWidgetState extends State { break; } children.add(InkWell( onTap: () => widget.parent!.setState(() { - widget.parent!.selected = day.toIso8601String(); - widget.parent!.selectedReal = ev.executionData; - if (widget.parent!.selectedReal == null) { + selected = day.toIso8601String(); + selectedReal = ev.executionData; + if (selectedReal == null) { widget.parent!.widget.isDayPlanner = true; } }), child: Container( + margin: const EdgeInsets.only(bottom: 2.5, top: 25), padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), @@ -146,8 +148,8 @@ class SchedulerCalendarWidgetState extends State { }), onDaySelected: (selectedDay, focusedDay) { widget.parent!.setState(() { - widget.parent!.selected = selectedDay.toIso8601String(); - widget.parent!.selectedReal = null; + selected = selectedDay.toIso8601String(); + selectedReal = null; widget.parent!.widget.isDayPlanner = true; }); }, diff --git a/lib/widgets/sheduler_items/scheduler_item.dart b/lib/widgets/sheduler_items/scheduler_item.dart index e182efa..be5a863 100644 --- a/lib/widgets/sheduler_items/scheduler_item.dart +++ b/lib/widgets/sheduler_items/scheduler_item.dart @@ -27,21 +27,21 @@ class SchedulerItemWidgetState extends State { widget.keys[ev.executionData!] = GlobalKey(); var d2 = DateTime.parse(ev.executionData!); DateTime? d3; - try { - d3 = DateTime.parse(ev.endDate!); + try { d3 = DateTime.parse(ev.endDate!); } catch (e) { /* */ } widgets.add(InkWell( onTap: () => widget.parent?.setState(() { - widget.parent?.selected = widget.parent?.selected != element ? element : null; - widget.parent?.selectedReal = widget.parent?.selected == null ? null : ev.executionData; - if (widget.parent!.selectedReal == null) { + selected = selected != element || ev.executionData != selectedReal ? element : null; + selectedReal = selected == null ? null : ev.executionData; + if (selectedReal == null) { widget.parent!.widget.isDayPlanner = true; } }), child: Container( key: widget.keys[ev.executionData!], padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 50), decoration: BoxDecoration( - border: widget.parent?.selected == element ? Border.all(color: const Color.fromRGBO(38, 166, 154, 1), width: 2) + border: selectedReal == ev.executionData ? + Border.all(color: const Color.fromRGBO(38, 166, 154, 1), width: 2) : Border(top: BorderSide(color: Colors.grey.shade300)), ), child: Row(children: [ @@ -79,37 +79,39 @@ class SchedulerItemWidgetState extends State { ))); } var date = DateTime.parse(element); - children.add(Column( children: [Container( - child: ExpansionTile( - enabled: widget.enabled, - shape: ContinuousRectangleBorder(), - iconColor: Colors.grey, - initiallyExpanded: true, - title: SizedBox( - child : Row( children: [ - const Padding(padding: EdgeInsets.only(right: 10), - child: Icon(Icons.view_day, color: Colors.grey)), - Flexible( - child: Padding( - padding: const EdgeInsets.only(right: 5), - child: Text("${date.day > 9 ? date.day : "0${date.day}"}-${date.hour > 9 ? date.hour : "0${date.hour}"}-${date.year}".toUpperCase(), overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w500)))) - ]) - ), - collapsedIconColor: Colors.grey, - children: widgets, - )), + children.add(Column( children: [ExpansionTile( + enabled: widget.enabled, + shape: const ContinuousRectangleBorder(), + iconColor: Colors.grey, + initiallyExpanded: true, + title: SizedBox( + child : Row( children: [ + const Padding(padding: EdgeInsets.only(right: 10), + child: Icon(Icons.view_day, color: Colors.grey)), + Flexible( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: Text("${date.day > 9 ? date.day : "0${date.day}"}-${date.month > 9 ? date.month : "0${date.month}"}-${date.year}".toUpperCase(), overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w500)))) + ]) + ), + collapsedIconColor: Colors.grey, + children: widgets, + ), Divider(color: Colors.grey.shade300, height: 1) ])); } Future.delayed( const Duration(milliseconds: 100), () { - if (widget.parent?.selectedReal != null) { - widget.keys[widget.parent!.selectedReal!]?.currentContext?.findRenderObject()?.showOnScreen(); + if (selectedReal != null) { + widget.keys[selectedReal!]?.currentContext?.findRenderObject()?.showOnScreen(); } }); - return SingleChildScrollView( child: Container( + return Container( + alignment: children.isNotEmpty ? Alignment.topLeft : Alignment.center, + color: children.isNotEmpty ? Colors.transparent : Colors.grey.shade300, + width: children.isNotEmpty ? MediaQuery.of(context).size.width : null, height: MediaQuery.of(context).size.height - HeaderConstants.height - 50, - child: Column( children: children)) + child: children.isNotEmpty ? SingleChildScrollView( child: Column( children: children)) : const Text("NO DATA FOUND", style: TextStyle(color: Colors.grey, fontSize: 30)) ); } } \ No newline at end of file diff --git a/library/flutter_flow_chart/lib/src/dashboard.dart b/library/flutter_flow_chart/lib/src/dashboard.dart index aa19e71..031d482 100755 --- a/library/flutter_flow_chart/lib/src/dashboard.dart +++ b/library/flutter_flow_chart/lib/src/dashboard.dart @@ -30,8 +30,7 @@ class Dashboard extends ChangeNotifier { List> tempHistory = []; List> history = []; Map scheduler = {}; - Map schedulerState = {}; - bool schedulerSave = false; + bool scheduleActive = false; String? id; String name; String defaultName = ""; @@ -108,6 +107,7 @@ class Dashboard extends ChangeNotifier { map['defaultArrowDirection'] as int? ?? 0], defaultArrowStyle: ArrowStyle.values[map['arrowStyle'] as int? ?? 0], ); + d..scheduleActive = map['schedule_active'] as bool? ?? false; d..arrows = List.from( (map['arrows'] as List).map( (x) => ArrowPainter.fromMap(x as Map), @@ -137,6 +137,7 @@ class Dashboard extends ChangeNotifier { } void copyFromMap(Map map) { + scheduleActive = map['schedule_active'] as bool? ?? false; scheduler = map['schedule'] as Map? ?? {}; defaultArrowStyle = ArrowStyle.values[map['arrowStyle'] as int? ?? 0]; defaultDashSpace = map['defaultDashSpace'] as double? ?? 0; @@ -222,17 +223,16 @@ class Dashboard extends ChangeNotifier { d["id"]=id; d["name"]=name; d["graph"]=graph; - if (schedulerSave) { - d["schedule"]=scheduler; - } + d["schedule"]=scheduler; + d["schedule_active"]=scheduleActive; return d; } void deserialize(Map graph) { elements = []; arrows = []; - print(graph['schedule']); scheduler = graph['schedule'] ?? {}; + scheduleActive = graph['schedule_active'] ?? false; setZoomFactor(graph["graph"]?["zoom"] ?? 1.0); for(var el in graph['graph']?['elements'] ?? []) { List nexts = []; diff --git a/library/flutter_flow_chart/lib/src/elements/flow_element.dart b/library/flutter_flow_chart/lib/src/elements/flow_element.dart index 677fafc..2fed2de 100755 --- a/library/flutter_flow_chart/lib/src/elements/flow_element.dart +++ b/library/flutter_flow_chart/lib/src/elements/flow_element.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_flow_chart/flutter_flow_chart.dart'; -import 'package:flutter_flow_chart/src/dashboard.dart'; import 'package:uuid/uuid.dart'; /// Kinf od element diff --git a/library/flutter_flow_chart/lib/src/flow_chart.dart b/library/flutter_flow_chart/lib/src/flow_chart.dart index eb9cfdd..0781cf7 100755 --- a/library/flutter_flow_chart/lib/src/flow_chart.dart +++ b/library/flutter_flow_chart/lib/src/flow_chart.dart @@ -19,6 +19,9 @@ import 'package:uuid/uuid.dart'; abstract class FlowData { String getID(); String getName(); + double? getWidth(); + double? getHeight(); + Map serialize(); FlowData deserialize(Map data); } @@ -297,6 +300,20 @@ class FlowChartState extends State { @override Widget build(BuildContext context) { + if (!widget.dashboard.isOpened && widget.onDashboardAlertOpened != null ) { + Future.delayed(Duration(milliseconds: 100), () { + showDialog( + barrierDismissible: false, + context: context, builder: (context) { + return AlertDialog( + titlePadding: EdgeInsets.zero, + insetPadding: EdgeInsets.zero, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0)), + title: widget.onDashboardAlertOpened!(context, widget.dashboard)); + + }); }); + } /// get dashboard position after first frame is drawn WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (mounted) { @@ -379,9 +396,6 @@ class FlowChartState extends State { if (change) { DrawingArrow.instance.notifyListeners(); widget.dashboard.chartKey.currentState?.setState(() { }); - /*Future.delayed(Duration(milliseconds: 10), () { - node.requestFocus(); - });*/ } }, child: ClipRect( @@ -421,12 +435,16 @@ class FlowChartState extends State { onAcceptWithDetails: (DragTargetDetails details) { var e = details.data; String newID = const Uuid().v4(); + double ratio = 1; + if (e.getWidth() != null && e.getHeight() != null) { + ratio = (e.getHeight()! / (e.getWidth()! - 30)); + } FlowElement el = FlowElement( dashboard: widget.dashboard, id: newID, element: e, position: details.offset, - size: const Size(100, 100), + size: Size(100, 100 * ratio), text: '${widget.dashboard.elements.length}', handlerSize: 15, widget: widget.itemWidget(e), @@ -477,33 +495,22 @@ class FlowChartState extends State { onDragEnd: (d) => node.requestFocus(), childWhenDragging: Opacity(opacity: .5, child: Padding( padding: const EdgeInsets.all(10), - child: Container( height: realSize - 20, child: widget.itemWidget(e) ))), - feedback: Container( height: realSize, child: widget.itemWidget(e) ), + child: Container( child: widget.itemWidget(e), + constraints: BoxConstraints(maxHeight: realSize - 20, maxWidth: realSize - 20), ))), + feedback: Container( constraints: BoxConstraints(maxHeight: realSize, maxWidth: realSize), + child: widget.itemWidget(e) ), child: InkWell( mouseCursor: SystemMouseCursors.grab, child: Padding( padding: const EdgeInsets.all(10), child: widget.itemWidgetTooltip != null ? HoverMenu( key: hoverKey, width: 400, title: Container( - height: realSize - 20, child: widget.itemWidget(e) ), + constraints: BoxConstraints( maxHeight: realSize - 20, maxWidth: realSize - 20), + child: widget.itemWidget(e) ), items: [ Container(child: widget.itemWidgetTooltip!(e)), ] ) : Container( - height: realSize - 20, child: widget.itemWidget(e) + constraints: BoxConstraints(maxHeight: realSize - 20, maxWidth: realSize - 20), child: widget.itemWidget(e) ) ) ))); } - if (!widget.dashboard.isOpened && widget.onDashboardAlertOpened != null ) { - Future.delayed(Duration(milliseconds: 100), () { - showDialog( - barrierDismissible: false, - context: context, builder: (context) { - return AlertDialog( - titlePadding: EdgeInsets.zero, - insetPadding: EdgeInsets.zero, - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0)), - title: widget.onDashboardAlertOpened!(context, widget.dashboard)); - - }); }); - } return res; } }