Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Ability to save 1 draft / flow and a creation draft #1235

Merged
merged 8 commits into from
May 12, 2023
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