Repository: kestra-io/kestra
Issue: Provide more comprehensive template for flow creation init
Labels: enhancement
frontend

### Feature description

It would be great to have this template instead of the existing one since flows need at least one task to be valid
```
id: ""
namespace: ""
tasks:
  - id: ""
    type: ""
```

Issue comments:
\+ removing quotes by default
+1 to quotes and also it would be nice to have it prefilled to make it even easier e.g. 

```yaml
id: helloThere
namespace: dev
tasks:
  - id: hello
    type: io.kestra.core.tasks.log.Log
    message: Kestra team wishes you a great day! 👋
```
what do you think? this way, opening the editor would be even more encouraging! :) 
+1 for the prefill

Pull request review comments:
not necessary?
This is false; only a not existing task type can prevent creating a graph.
 
Oups, I was testing something, good spot :+1: 
In fact I was wondering why this was red and was ensuring array had this method. 
![image](https://github.com/kestra-io/kestra/assets/37618489/ff51891a-5695-4c7b-b668-2cc6633f0aa3)

Looks to be related to this but idk if we should add something to the compiler https://github.com/Microsoft/TypeScript/issues/2340

I don't have the issue; maybe you have some dependencies that are not up to date? 
I feel like we weren't handling that case unless in creation mode. Is that intended ? Should we create another issue ? We may need to add another validate then or should we simply call fetchGraph and handle the error case ?
I just npm installed and still have the issue, looks strange :thinking: 
I guess I will try to live along with this compiler error

Patch that resolved the issue:
diff --git a/ui/src/components/flows/FlowCreate.vue b/ui/src/components/flows/FlowCreate.vue
index 3d62e96d57..a12306fc95 100644
--- a/ui/src/components/flows/FlowCreate.vue
+++ b/ui/src/components/flows/FlowCreate.vue
@@ -1,39 +1,51 @@
 <template>
     <div>
-        <Topology
-            :flow-id="flowId"
-            :namespace="namespace"
+        <editor-view
+            :flow-id="defaultFlowTemplate.id"
+            :namespace="defaultFlowTemplate.namespace"
             :is-creating="true"
             :flow-graph="flowGraph"
             :is-read-only="false"
             :total="total"
             :guided-properties="guidedProperties"
             :flow-error="flowError"
+            :flow="flowWithSource"
         />
     </div>
     <div id="guided-right" />
 </template>
 
 <script>
-    import Topology from "../inputs/EditorView.vue";
+    import EditorView from "../inputs/EditorView.vue";
     import {mapGetters, mapState} from "vuex";
     import RouteContext from "../../mixins/routeContext";
+    import YamlUtils from "../../utils/yamlUtils";
 
     export default {
         mixins: [RouteContext],
         components: {
-            Topology
+            EditorView
         },
         data() {
             return {
-                flowId: "",
-                namespace: "",
+                defaultFlowTemplate: {
+                    id: "hello-world",
+                    namespace: "dev",
+                    tasks: [{
+                        id: "hello",
+                        type: "io.kestra.core.tasks.log.Log",
+                        message: "Kestra team wishes you a great day! 👋"
+                    }]
+                }
             }
         },
         beforeUnmount() {
             this.$store.commit("flow/setFlowError", undefined);
         },
         computed: {
+            flowWithSource() {
+                return {source: YamlUtils.stringify(this.defaultFlowTemplate)};
+            },
             ...mapState("flow", ["flowGraph", "total"]),
             ...mapState("auth", ["user"]),
             ...mapState("plugin", ["pluginSingleList", "pluginsDocumentation"]),
diff --git a/ui/src/components/graph/nodes/Edge.vue b/ui/src/components/graph/nodes/Edge.vue
index b52e704d74..35d0f7030e 100644
--- a/ui/src/components/graph/nodes/Edge.vue
+++ b/ui/src/components/graph/nodes/Edge.vue
@@ -106,11 +106,11 @@
 
     const path = computed(() => getSmoothStepPath(props))
 
-    const onMouseOver = (edge) => {
+    const onMouseOver = () => {
         isHover.value = true;
     }
 
-    const onMouseLeave = (edge) => {
+    const onMouseLeave = () => {
         isHover.value = false;
     }
 
@@ -118,7 +118,7 @@
         taskYaml.value = task;
         clearTimeout(timer.value);
         timer.value = setTimeout(() => {
-            store.dispatch("flow/validateTask", {task: task})
+            store.dispatch("flow/validateTask", {task: task, section: SECTIONS.TASKS})
         }, 500);
     }
 
@@ -142,7 +142,7 @@
     }
 
     const taskHaveId = () => {
-        return taskYaml.value.length > 0 ? YamlUtils.parse(taskYaml.value).id ? true : false : false;
+        return taskYaml.value.length > 0 ? !!YamlUtils.parse(taskYaml.value).id : false;
     }
 
     const checkTaskExist = () => {
diff --git a/ui/src/components/inputs/EditorView.vue b/ui/src/components/inputs/EditorView.vue
index f9dc1f669c..ac38d6b54c 100644
--- a/ui/src/components/inputs/EditorView.vue
+++ b/ui/src/components/inputs/EditorView.vue
@@ -20,9 +20,9 @@
     import permission from "../../models/permission";
     import action from "../../models/action";
     import YamlUtils from "../../utils/yamlUtils";
-    import taskEditor from "../flows/TaskEditor.vue";
-    import metadataEditor from "../flows/MetadataEditor.vue";
-    import editor from "./Editor.vue";
+    import TaskEditor from "../flows/TaskEditor.vue";
+    import MetadataEditor from "../flows/MetadataEditor.vue";
+    import Editor from "./Editor.vue";
     import yamlUtils from "../../utils/yamlUtils";
     import {pageFromRoute} from "../../utils/eventsRouter";
     import {SECTIONS} from "../../utils/constants.js";
@@ -163,22 +163,9 @@
     }
 
     const initYamlSource = async () => {
-        flowYaml.value = props.flow ? props.flow.source : YamlUtils.stringify({
-            id: props.flowId,
-            namespace: props.namespace
-        });
+        flowYaml.value = props.flow.source;
 
-        if(!props.isCreating && !props.isReadOnly) {
-            const validation = await store.dispatch("flow/validateFlow", {flow: flowYaml.value});
-            const validationErrors = validation[0].constraints;
-            if (validationErrors) {
-                singleErrorToast(t("cannot create topology"), t("invalid flow"), validationErrors);
-            }else {
-                generateGraph();
-            }
-        } else {
-            generateGraph();
-        }
+        await generateGraph();
 
         if(!props.isReadOnly) {
             let restoredLocalStorageKey;
@@ -204,9 +191,6 @@
     }
 
     onMounted(async () => {
-        if (props.isCreating) {
-            store.commit("flow/setFlowGraph", undefined);
-        }
         await initYamlSource();
         // Save on ctrl+s in topology
         document.addEventListener("keydown", save);
@@ -343,6 +327,16 @@
         persistEditorContent(false);
     }
 
+    const fetchGraph = async (yaml) => {
+        await store.dispatch("flow/loadGraphFromSource", {
+            flow: yaml, config: {
+                validateStatus: (status) => {
+                    return status === 200 || status === 422;
+                }
+            }
+        })
+    }
+
     const onEdit = (event) => {
         flowYaml.value = event;
         haveChange.value = true;
@@ -350,33 +344,20 @@
         return store.dispatch("flow/validateFlow", {flow: event})
             .then(value => {
                 if (flowHaveTasks() && ["topology", "source-topology"].includes(viewType.value)) {
-                    store.dispatch("flow/loadGraphFromSource", {
-                        flow: event, config: {
-                            validateStatus: (status) => {
-                                return status === 200 || status === 422;
-                            }
-                        }
-                    })
-                } else {
-                    regenerateGraph();
+                    generateGraph()
                 }
 
                 return value;
             });
     }
 
-    const generateGraph = () => {
+    const generateGraph = async () => {
+        await fetchGraph(flowYaml.value);
         if (props.flowGraph) {
             lowCodeEditorRef.value.generateGraph();
         }
     }
 
-    const regenerateGraph = () => {
-        if (props.flowGraph) {
-            lowCodeEditorRef.value.regenerateGraph();
-        }
-    }
-
     const loadingState = (value) => {
         isLoading.value = value;
     }
@@ -821,7 +802,7 @@
                     @click="save"
                     v-if="isAllowedEdit"
                     :type="flowError ? 'danger' : 'primary'"
-                    :disabled="!haveChange"
+                    :disabled="!haveChange && !isCreating"
                     class="edit-flow-save-button"
                 >
                     {{ $t("save") }}
diff --git a/ui/src/components/inputs/LowCodeEditor.vue b/ui/src/components/inputs/LowCodeEditor.vue
index 65e905c9d7..b1790eaffe 100644
--- a/ui/src/components/inputs/LowCodeEditor.vue
+++ b/ui/src/components/inputs/LowCodeEditor.vue
@@ -105,7 +105,7 @@
     })
 
     watch(() => props.flowGraph, () => {
-        regenerateGraph();
+        generateGraph();
     })
 
     // Event listeners & Watchers
@@ -113,7 +113,7 @@
         const resizeObserver = new ResizeObserver(function () {
             clearTimeout(timer.value);
             timer.value = setTimeout(() => {
-                regenerateGraph();
+                generateGraph();
                 nextTick(() => {
                     fitView()
                 })
@@ -258,7 +258,7 @@
             localStorage.getItem("topology-orientation") !== "0" ? "0" : "1"
         );
         isHorizontal.value = localStorage.getItem("topology-orientation") === "1";
-        regenerateGraph();
+        generateGraph();
         fitView();
     };
 
@@ -370,171 +370,170 @@
         return target
     }
 
+    const cleanGraph = () => {
+        removeEdges(getEdges.value)
+        removeNodes(getNodes.value)
+        removeSelectedElements(getElements.value)
+        elements.value = []
+    }
+
     const generateGraph = () => {
-        emit("loading", true);
-        try {
-            if (!props.flowGraph || !flowHaveTasks()) {
-                elements.value.push({
-                    id: "start",
-                    label: "",
-                    type: "dot",
-                    position: {x: 0, y: 0},
-                    style: {
-                        width: "5px",
-                        height: "5px"
-                    },
-                    sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
-                    targetPosition: isHorizontal.value ? Position.Left : Position.Top,
-                    parentNode: undefined,
-                    draggable: false,
-                })
-                elements.value.push({
-                    id: "end",
-                    label: "",
-                    type: "dot",
-                    position: isHorizontal.value ? {x: 50, y: 0} : {x: 0, y: 50},
-                    style: {
-                        width: "5px",
-                        height: "5px"
-                    },
-                    sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
-                    targetPosition: isHorizontal.value ? Position.Left : Position.Top,
-                    parentNode: undefined,
-                    draggable: false,
-                })
-                elements.value.push({
-                    id: "start|end",
-                    source: "start",
-                    target: "end",
-                    type: "edge",
-                    markerEnd: MarkerType.ArrowClosed,
-                    data: {
-                        edge: {
-                            relation: {
-                                relationType: "SEQUENTIAL"
-                            }
+        cleanGraph();
+
+        nextTick(() => {
+            emit("loading", true);
+            try {
+                if (!props.flowGraph || !flowHaveTasks()) {
+                    elements.value.push({
+                        id: "start",
+                        label: "",
+                        type: "dot",
+                        position: {x: 0, y: 0},
+                        style: {
+                            width: "5px",
+                            height: "5px"
                         },
-                        isFlowable: false,
-                        initTask: true,
-                    }
-                })
+                        sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
+                        targetPosition: isHorizontal.value ? Position.Left : Position.Top,
+                        parentNode: undefined,
+                        draggable: false,
+                    })
+                    elements.value.push({
+                        id: "end",
+                        label: "",
+                        type: "dot",
+                        position: isHorizontal.value ? {x: 50, y: 0} : {x: 0, y: 50},
+                        style: {
+                            width: "5px",
+                            height: "5px"
+                        },
+                        sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
+                        targetPosition: isHorizontal.value ? Position.Left : Position.Top,
+                        parentNode: undefined,
+                        draggable: false,
+                    })
+                    elements.value.push({
+                        id: "start|end",
+                        source: "start",
+                        target: "end",
+                        type: "edge",
+                        markerEnd: MarkerType.ArrowClosed,
+                        data: {
+                            edge: {
+                                relation: {
+                                    relationType: "SEQUENTIAL"
+                                }
+                            },
+                            isFlowable: false,
+                            initTask: true,
+                        }
+                    })
 
-                emit("loading", false);
-                return;
-            }
-            if (props.flowGraph === undefined) {
+                    emit("loading", false);
+                    return;
+                }
+                if (props.flowGraph === undefined) {
+                    emit("loading", false);
+                    return;
+                }
+                const dagreGraph = generateDagreGraph();
+                const clusters = {};
+                for (let cluster of (props.flowGraph.clusters || [])) {
+                    for (let nodeUid of cluster.nodes) {
+                        clusters[nodeUid] = cluster.cluster;
+                    }
 
-                emit("loading", false);
-                return;
-            }
-            const dagreGraph = generateDagreGraph();
-            const clusters = {};
-            for (let cluster of (props.flowGraph.clusters || [])) {
-                for (let nodeUid of cluster.nodes) {
-                    clusters[nodeUid] = cluster.cluster;
+                    const dagreNode = dagreGraph.node(cluster.cluster.uid)
+                    const parentNode = cluster.parents ? cluster.parents[cluster.parents.length - 1] : undefined;
+
+                    const clusterUid = cluster.cluster.uid;
+                    elements.value.push({
+                        id: clusterUid,
+                        label: clusterUid,
+                        type: "cluster",
+                        parentNode: parentNode,
+                        position: getNodePosition(dagreNode, parentNode ? dagreGraph.node(parentNode) : undefined),
+                        style: {
+                            width: clusterUid === "Triggers" && isHorizontal.value ? "400px" : dagreNode.width + "px",
+                            height: clusterUid === "Triggers" && !isHorizontal.value ? "250px" : dagreNode.height + "px",
+                        },
+                    })
                 }
 
-                const dagreNode = dagreGraph.node(cluster.cluster.uid)
-                const parentNode = cluster.parents ? cluster.parents[cluster.parents.length - 1] : undefined;
-
-                const clusterUid = cluster.cluster.uid;
-                elements.value.push({
-                    id: clusterUid,
-                    label: clusterUid,
-                    type: "cluster",
-                    parentNode: parentNode,
-                    position: getNodePosition(dagreNode, parentNode ? dagreGraph.node(parentNode) : undefined),
-                    style: {
-                        width: clusterUid === "Triggers" && isHorizontal.value ? "400px" : dagreNode.width + "px",
-                        height: clusterUid === "Triggers" && !isHorizontal.value ? "250px" : dagreNode.height + "px",
-                    },
-                })
-            }
+                let disabledLowCode = [];
+
+                for (const node of props.flowGraph.nodes) {
+                    const dagreNode = dagreGraph.node(node.uid);
+                    let nodeType = "task";
+                    if (node.type.includes("GraphClusterEnd")) {
+                        nodeType = "dot";
+                    } else if (clusters[node.uid] === undefined && node.type.includes("GraphClusterRoot")) {
+                        nodeType = "dot";
+                    } else if (node.type.includes("GraphClusterRoot")) {
+                        nodeType = "dot";
+                    } else if (node.type.includes("GraphTrigger")) {
+                        nodeType = "trigger";
+                    }
+                    // Disable interaction for Dag task
+                    // because our low code editor can not handle it for now
+                    if (isTaskNode(node) && node.task.type === "io.kestra.core.tasks.flows.Dag") {
+                        disabledLowCode.push(node.task.id);
+                        YamlUtils.getChildrenTasks(props.source, node.task.id).forEach(child => {
+                            disabledLowCode.push(child);
+                        })
+                    }
 
-            let disabledLowCode = [];
-
-            for (const node of props.flowGraph.nodes) {
-                const dagreNode = dagreGraph.node(node.uid);
-                let nodeType = "task";
-                if (node.type.includes("GraphClusterEnd")) {
-                    nodeType = "dot";
-                } else if (clusters[node.uid] === undefined && node.type.includes("GraphClusterRoot")) {
-                    nodeType = "dot";
-                } else if (node.type.includes("GraphClusterRoot")) {
-                    nodeType = "dot";
-                } else if (node.type.includes("GraphTrigger")) {
-                    nodeType = "trigger";
-                }
-                // Disable interaction for Dag task
-                // because our low code editor can not handle it for now
-                if (isTaskNode(node) && node.task.type === "io.kestra.core.tasks.flows.Dag") {
-                    disabledLowCode.push(node.task.id);
-                    YamlUtils.getChildrenTasks(props.source, node.task.id).forEach(child => {
-                        disabledLowCode.push(child);
+                    elements.value.push({
+                        id: node.uid,
+                        label: isTaskNode(node) ? node.task.id : "",
+                        type: nodeType,
+                        position: getNodePosition(dagreNode, clusters[node.uid] ? dagreGraph.node(clusters[node.uid].uid) : undefined),
+                        style: {
+                            width: getNodeWidth(node) + "px",
+                            height: getNodeHeight(node) + "px"
+                        },
+                        sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
+                        targetPosition: isHorizontal.value ? Position.Left : Position.Top,
+                        parentNode: clusters[node.uid] ? clusters[node.uid].uid : undefined,
+                        draggable: nodeType === "task" && !props.isReadOnly && isTaskNode(node) ? !disabledLowCode.includes(node.task.id) : false,
+                        data: {
+                            node: node,
+                            namespace: props.namespace,
+                            flowId: props.flowId,
+                            revision: props.execution ? props.execution.flowRevision : undefined,
+                            isFlowable: isTaskNode(node) ? flowables().includes(node.task.id) : false
+                        },
                     })
                 }
 
-                elements.value.push({
-                    id: node.uid,
-                    label: isTaskNode(node) ? node.task.id : "",
-                    type: nodeType,
-                    position: getNodePosition(dagreNode, clusters[node.uid] ? dagreGraph.node(clusters[node.uid].uid) : undefined),
-                    style: {
-                        width: getNodeWidth(node) + "px",
-                        height: getNodeHeight(node) + "px"
-                    },
-                    sourcePosition: isHorizontal.value ? Position.Right : Position.Bottom,
-                    targetPosition: isHorizontal.value ? Position.Left : Position.Top,
-                    parentNode: clusters[node.uid] ? clusters[node.uid].uid : undefined,
-                    draggable: nodeType === "task" && !props.isReadOnly && isTaskNode(node) ? !disabledLowCode.includes(node.task.id) : false,
-                    data: {
-                        node: node,
-                        namespace: props.namespace,
-                        flowId: props.flowId,
-                        revision: props.execution ? props.execution.flowRevision : undefined,
-                        isFlowable: isTaskNode(node) ? flowables().includes(node.task.id) : false
-                    },
-                })
+                for (const edge of props.flowGraph.edges) {
+                    elements.value.push({
+                        id: edge.source + "|" + edge.target,
+                        source: edge.source,
+                        target: edge.target,
+                        type: "edge",
+                        markerEnd: MarkerType.ArrowClosed,
+                        data: {
+                            edge: edge,
+                            haveAdd: complexEdgeHaveAdd(edge),
+                            isFlowable: flowables().includes(edge.source) || flowables().includes(edge.target),
+                            nextTaskId: getNextTaskId(edge.target),
+                            disabled: disabledLowCode.includes(edge.source)
+                        }
+                    })
+                }
+            } catch (e) {
+                console.error("Error while creating topology graph: " + e);
             }
-
-            for (const edge of props.flowGraph.edges) {
-                elements.value.push({
-                    id: edge.source + "|" + edge.target,
-                    source: edge.source,
-                    target: edge.target,
-                    type: "edge",
-                    markerEnd: MarkerType.ArrowClosed,
-                    data: {
-                        edge: edge,
-                        haveAdd: complexEdgeHaveAdd(edge),
-                        isFlowable: flowables().includes(edge.source) || flowables().includes(edge.target),
-                        nextTaskId: getNextTaskId(edge.target),
-                        disabled: disabledLowCode.includes(edge.source)
-                    }
-                })
+            finally {
+                emit("loading", false);
             }
-        } catch (e) {
-            console.error("Error while creating topology graph: " + e);
-        }
-        finally {
-            emit("loading", false);
-        }
-    }
-
-    const regenerateGraph = () => {
-        removeEdges(getEdges.value)
-        removeNodes(getNodes.value)
-        removeSelectedElements(getElements.value)
-        elements.value = []
-        nextTick(() => {
-            generateGraph();
         })
     }
 
     // Expose method to be triggered by parents
     defineExpose({
-        generateGraph,
-        regenerateGraph
+        generateGraph
     })
 </script>
 


Test patch: