Skip to content

Commit

Permalink
feat(ui): Save as draft on invalid flows (#1235)
Browse files Browse the repository at this point in the history
There is now 1 draft / namespace.flowId & 1 creation draft

closes #1151
  • Loading branch information
brian-mulier-p authored May 12, 2023
1 parent 3cf551c commit 5628309
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 51 deletions.
128 changes: 87 additions & 41 deletions ui/src/components/graph/Topology.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, computed} from "vue";
import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, computed} from "vue";
import {useStore} from "vuex"
import {VueFlow, useVueFlow, Position, MarkerType} from "@vue-flow/core"
import {Controls, ControlButton} from "@vue-flow/controls"
Expand Down Expand Up @@ -128,6 +128,10 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
const taskError = ref(store.getters["flow/taskError"])
const user = store.getters["auth/user"];
const localStorageKey = computed(() => {
return (props.isCreating ? "creation" : `${flow.namespace}.${flow.id}`) + "_draft";
})
watch(() => store.getters["flow/taskError"], async () => {
taskError.value = store.getters["flow/taskError"];
});
Expand Down Expand Up @@ -177,7 +181,19 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
id: props.flowId,
namespace: props.namespace
});
return flowYaml.value;
generateGraph();
if(!props.isReadOnly) {
const sourceFromLocalStorage = localStorage.getItem(localStorageKey.value);
if (sourceFromLocalStorage !== null) {
toast.confirm(props.isCreating ? t("save draft.retrieval.creation") : t("save draft.retrieval.existing", {flowFullName: `${flow.namespace}.${flow.id}`}), () => {
flowYaml.value = sourceFromLocalStorage;
onEdit(flowYaml.value);
})
localStorage.removeItem(localStorageKey.value);
}
}
}
const isTaskNode = (node) => {
Expand Down Expand Up @@ -209,13 +225,15 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
};
const regenerateGraph = () => {
removeEdges(getEdges.value)
removeNodes(getNodes.value)
removeSelectedElements(getElements.value)
elements.value = []
nextTick(() => {
generateGraph();
})
if(!props.flowError) {
removeEdges(getEdges.value)
removeNodes(getNodes.value)
removeSelectedElements(getElements.value)
elements.value = []
nextTick(() => {
generateGraph();
})
}
}
const toggleOrientation = () => {
Expand Down Expand Up @@ -417,7 +435,6 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
store.commit("flow/setFlowGraph", undefined);
}
initYamlSource();
generateGraph();
// Regenerate graph on window resize
observeWidth();
// Save on ctrl+s in topology
Expand Down Expand Up @@ -535,30 +552,56 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
}
};
const showDraftPopup = (draftReason, refreshAfterSelect = false) => {
toast.confirm(draftReason + " " + (props.isCreating ? t("save draft.prompt.creation") : t("save draft.prompt.existing", {
namespace: flow.namespace,
id: flow.id
})),
() => {
localStorage.setItem(localStorageKey.value, flowYaml.value);
store.dispatch("core/isUnsaved", false);
if(refreshAfterSelect){
router.go();
}else {
router.push({
name: "flows/list"
})
}
},
() => {
if(refreshAfterSelect) {
store.dispatch("core/isUnsaved", false);
router.go();
}
}
)
}
const onEdit = (event) => {
store.dispatch("flow/validateFlow", {flow: event})
return store.dispatch("flow/validateFlow", {flow: event})
.then(value => {
if (value[0].constraints && !flowHaveTasks(event)) {
flowYaml.value = event;
haveChange.value = true;
} else {
flowYaml.value = event;
haveChange.value = true;
store.dispatch("core/isUnsaved", true);
if (!value[0].constraints) {
// flowYaml need to be update before
// loadGraphFromSource to avoid
// generateGraph to be triggered with the old value
flowYaml.value = event;
store.dispatch("flow/loadGraphFromSource", {
flow: event, config: {
validateStatus: (status) => {
return status === 200 || status === 422;
}
}
}).then(response => {
haveChange.value = true;
store.dispatch("core/isUnsaved", true);
})
}
})
return value;
}).catch(e => {
showDraftPopup(t("save draft.prompt.http_error"), true);
return Promise.reject(e);
})
}
const onDelete = (event) => {
Expand Down Expand Up @@ -740,7 +783,6 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
const editorUpdate = (event) => {
updatedFromEditor.value = true;
flowYaml.value = event;
haveChange.value = true;
clearTimeout(timer.value);
timer.value = setTimeout(() => onEdit(event), 500);
Expand Down Expand Up @@ -780,9 +822,18 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
tours["guidedTour"].nextStep();
return;
}
if (props.isCreating) {
const flowParsed = YamlUtils.parse(flowYaml.value);
if (flowParsed.id && flowParsed.namespace) {
onEdit(flowYaml.value).then(validation => {
let flowParsed;
try {
flowParsed = YamlUtils.parse(flowYaml.value);
} catch (_) {}
if (validation[0].constraints) {
showDraftPopup(t("save draft.prompt.base"));
return;
}
if (props.isCreating) {
store.dispatch("flow/createFlow", {flow: flowYaml.value})
.then((response) => {
toast.saved(response.id);
Expand All @@ -792,22 +843,17 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
params: {id: flowParsed.id, namespace: flowParsed.namespace, tab: "editor"}
});
})
return;
} else {
store.dispatch("core/showMessage", {
variant: "error",
title: t("can not save"),
message: t("flow must have id and namespace")
});
return;
}else {
store
.dispatch("flow/saveFlow", {flow: flowYaml.value})
.then((response) => {
toast.saved(response.id);
store.dispatch("core/isUnsaved", false);
})
}
}
store
.dispatch("flow/saveFlow", {flow: flowYaml.value})
.then((response) => {
toast.saved(response.id);
store.dispatch("core/isUnsaved", false);
})
haveChange.value = false;
})
};
const canExecute = () => {
Expand Down Expand Up @@ -1129,8 +1175,8 @@ import {ref, onMounted, nextTick, watch, getCurrentInstance, onBeforeUnmount, co
size="large"
@click="save"
v-if="isAllowedEdit"
:disabled="!haveChange || !flowHaveTasks()"
type="primary"
:type="flowError || !haveChange ? 'danger' : 'primary'"
:disabled="!haveChange"
class="edit-flow-save-button"
>
{{ $t("save") }}
Expand Down
3 changes: 3 additions & 0 deletions ui/src/components/templates/TemplateEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
.dispatch("template/loadTemplate", this.$route.params)
.then(this.loadFile);
}
},
onChange() {
this.$store.dispatch("core/isUnsaved", this.previousContent !== this.content);
}
}
};
Expand Down
3 changes: 0 additions & 3 deletions ui/src/mixins/flowTemplateEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,6 @@ export default {
});
}
},
onChange() {
this.$store.dispatch("core/isUnsaved", this.previousContent !== this.content);
},
save() {
if (this.$tours["guidedTour"].isRunning.value && !this.guidedProperties.saveFlow) {
this.$store.dispatch("api/events", {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/stores/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export default {
return this.$http.delete("/api/v1/flows/delete/by-query", options, {params: options})
},
validateFlow({commit}, options) {
return axios.post(`${apiRoot}flows/validate`, options.flow, textYamlHeader)
return this.$http.post(`${apiRoot}flows/validate`, options.flow, textYamlHeader)
.then(response => {
commit("setFlowError", response.data[0] ? response.data[0].constraints : undefined)
return response.data
Expand Down
28 changes: 26 additions & 2 deletions ui/src/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,19 @@
"error in editor": "An error have been found in the editor",
"delete task confirm": "Do you want to delete the task <code>{taskId}</code> ?",
"can not save": "Can not save",
"flow must have id and namespace": "Flow must have an id and a namespace."
"flow must have id and namespace": "Flow must have an id and a namespace.",
"save draft": {
"prompt": {
"base": "The current Flow is not valid. Do you still want to save it as a draft ?",
"http_error": "Unable to validate the Flow due to an HTTP error. Do you want to save the current Flow as draft ?",
"creation": "You will retrieve it on your next Flow creation.",
"existing": "You will retrieve it on your next <code>{namespace}.{id}</code> Flow edition."
},
"retrieval": {
"creation": "A Flow creation draft was retrieved, do you want to resume its edition ?",
"existing": "A <code>{flowFullName}</code> Flow draft was retrieved, do you want to resume its edition ?"
}
}
},
"fr": {
"id": "Identifiant",
Expand Down Expand Up @@ -816,6 +828,18 @@
"error in editor": "Une erreur a été trouvé dans l'éditeur",
"delete task confirm": "Êtes-vous sûr de vouloir supprimer la tâche <code>{taskId}</code> ?",
"can not save": "Impossible de sauvegarder",
"flow must have id and namespace": "Le flow doit avoir un id et un namespace."
"flow must have id and namespace": "Le flow doit avoir un id et un namespace.",
"save draft": {
"prompt": {
"base": "Le Flow actuel est invalide. Voulez-vous le sauvegarder en tant que brouillon ?",
"http_error": "Impossible de valider le Flow en raison d'une erreur HTTP. Voulez-vous sauvegarder le Flow actuel en tant que brouillon ?",
"creation": "Vous le récupérerez à votre prochaine création de Flow.",
"existing": "Vous le récupérerez à votre prochaine édition du Flow <code>{namespace}.{id}</code>."
},
"retrieval": {
"creation": "Un brouillon de création de Flow existe, voulez-vous reprendre son édition ?",
"existing": "Un brouillon pour le Flow <code>{flowFullName}</code> existe, voulez-vous reprendre son édition?"
}
}
}
}
9 changes: 7 additions & 2 deletions ui/src/utils/axios.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ export default (callback, store, router) => {
return Promise.reject(errorResponse);
}

if (errorResponse.response.status === 401 && !store.getters["auth/isLogged"]) {
if (errorResponse.response.status === 401 && (!store.getters["auth/isLogged"] || store.getters["auth/expired"])) {
window.location = "/ui/login?from=" + window.location.pathname +
(window.location.search ? "?" + window.location.search : "")

}

if (errorResponse.response.status === 401 &&
Expand All @@ -111,6 +110,7 @@ export default (callback, store, router) => {

return Promise.reject(errorResponse);
}

if (errorResponse.response.status === 400){
return Promise.reject(errorResponse.response.data)
}
Expand All @@ -122,6 +122,11 @@ export default (callback, store, router) => {
variant: "error"
})

if(errorResponse.response.status === 401 &&
store.getters["auth/isLogged"]){
store.commit("auth/setExpired", true);
}

return Promise.reject(errorResponse);
}

Expand Down
4 changes: 2 additions & 2 deletions ui/src/utils/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default {
return h("span", {innerHTML: message});
}
},
confirm: function(message, callback) {
confirm: function(message, callback, callbackIfCancel = () => {}) {
ElMessageBox.confirm(
this._wrap(message || self.$t("toast confirm")),
self.$t("confirmation"),
Expand All @@ -39,7 +39,7 @@ export default {
callback();
})
.catch(() => {

callbackIfCancel()
})
},
saved: function(name, title, options) {
Expand Down

0 comments on commit 5628309

Please sign in to comment.