fix: avoid blocking creation of flow when edition is restricted to a namespace (#13694)

This commit is contained in:
Barthélémy Ledoux
2025-12-16 14:24:16 +01:00
committed by GitHub
parent 67ada7f61b
commit abcf76f7b4
7 changed files with 52 additions and 48 deletions

View File

@@ -20,6 +20,9 @@
import {useVueTour} from "../../composables/useVueTour"; import {useVueTour} from "../../composables/useVueTour";
import type {BlueprintType} from "../../stores/blueprints" import type {BlueprintType} from "../../stores/blueprints"
import {useAuthStore} from "../../override/stores/auth";
import permission from "../../models/permission";
import action from "../../models/action";
const route = useRoute(); const route = useRoute();
const {t} = useI18n(); const {t} = useI18n();
@@ -29,13 +32,21 @@
const blueprintsStore = useBlueprintsStore(); const blueprintsStore = useBlueprintsStore();
const coreStore = useCoreStore(); const coreStore = useCoreStore();
const flowStore = useFlowStore(); const flowStore = useFlowStore();
const authStore = useAuthStore();
const setupFlow = async () => { const setupFlow = async () => {
const blueprintId = route.query.blueprintId as string; const blueprintId = route.query.blueprintId as string;
const blueprintSource = route.query.blueprintSource as BlueprintType; const blueprintSource = route.query.blueprintSource as BlueprintType;
const implicitDefaultNamespace = authStore.user.getNamespacesForAction(
permission.FLOW,
action.CREATE,
)[0];
let flowYaml = ""; let flowYaml = "";
const id = getRandomID(); const id = getRandomID();
const selectedNamespace = (route.query.namespace as string) || defaultNamespace() || "company.team"; const selectedNamespace = (route.query.namespace as string)
?? defaultNamespace()
?? implicitDefaultNamespace
?? "company.team";
if (route.query.copy && flowStore.flow) { if (route.query.copy && flowStore.flow) {
flowYaml = flowStore.flow.source; flowYaml = flowStore.flow.source;

View File

@@ -1,7 +1,7 @@
<template> <template>
<span ref="rootContainer"> <span ref="rootContainer">
<!-- Valid --> <!-- Valid -->
<el-button v-if="!errors && !warnings &&!infos" v-bind="$attrs" :link="link" :size="size" type="default" class="success square" disabled> <el-button v-if="!errors && !warnings && !infos" v-bind="$attrs" :link="link" :size="size" type="default" class="success square" disabled>
<CheckBoldIcon class="text-success" /> <CheckBoldIcon class="text-success" />
</el-button> </el-button>
@@ -157,6 +157,7 @@
} }
&.success { &.success {
cursor: default;
border-color: var(--ks-border-success); border-color: var(--ks-border-success);
} }

View File

@@ -5,19 +5,19 @@
<ValidationError <ValidationError
class="validation" class="validation"
tooltipPlacement="bottom-start" tooltipPlacement="bottom-start"
:errors="flowErrors" :errors="flowStore.flowErrors"
:warnings="flowWarnings" :warnings="flowWarnings"
:infos="flowInfos" :infos="flowStore.flowInfos"
/> />
<EditorButtons <EditorButtons
:isCreating="flowStore.isCreating" :isCreating="flowStore.isCreating"
:isReadOnly="isReadOnly" :isReadOnly="flowStore.isReadOnly"
:canDelete="true" :canDelete="true"
:isAllowedEdit="isAllowedEdit" :isAllowedEdit="flowStore.isAllowedEdit"
:haveChange="haveChange" :haveChange="haveChange"
:flowHaveTasks="Boolean(flowHaveTasks)" :flowHaveTasks="Boolean(flowStore.flowHaveTasks)"
:errors="flowErrors" :errors="flowStore.flowErrors"
:warnings="flowWarnings" :warnings="flowWarnings"
@save="save" @save="save"
@copy=" @copy="
@@ -49,7 +49,6 @@
import ValidationError from "../flows/ValidationError.vue"; import ValidationError from "../flows/ValidationError.vue";
import localUtils from "../../utils/utils"; import localUtils from "../../utils/utils";
import {useFlowOutdatedErrors} from "./flowOutdatedErrors";
import {useFlowStore} from "../../stores/flow"; import {useFlowStore} from "../../stores/flow";
import {useToast} from "../../utils/toast"; import {useToast} from "../../utils/toast";
@@ -73,22 +72,14 @@
const route = useRoute() const route = useRoute()
const routeParams = computed(() => route.params) const routeParams = computed(() => route.params)
const {translateError, translateErrorWithKey} = useFlowOutdatedErrors();
// If playground is not defined, enable it by default // If playground is not defined, enable it by default
const isSettingsPlaygroundEnabled = computed(() => localStorage.getItem("editorPlayground") === "false" ? false : true); const isSettingsPlaygroundEnabled = computed(() => localStorage.getItem("editorPlayground") === "false" ? false : true);
const isReadOnly = computed(() => flowStore.isReadOnly)
const isAllowedEdit = computed(() => flowStore.isAllowedEdit)
const flowHaveTasks = computed(() => flowStore.flowHaveTasks)
const flowErrors = computed(() => flowStore.flowErrors?.map(translateError));
const flowInfos = computed(() => flowStore.flowInfos)
const toast = useToast(); const toast = useToast();
const flowWarnings = computed(() => { const flowWarnings = computed(() => {
const outdatedWarning = const outdatedWarning =
flowStore.flowValidation?.outdated && !flowStore.isCreating flowStore.flowValidation?.outdated && !flowStore.isCreating
? [translateErrorWithKey(flowStore.flowValidation?.constraints ?? "")] ? flowStore.flowValidation?.constraints?.split(", ") ?? []
: []; : [];
const deprecationWarnings = const deprecationWarnings =

View File

@@ -1,22 +0,0 @@
import {useI18n} from "vue-i18n";
export function useFlowOutdatedErrors(){
const {t} = useI18n();
function translateError(error: string): string {
if(error.startsWith(">>>>")){
const key = error.substring(4).trim();
return translateErrorWithKey(key);
} else {
return error;
}
}
function translateErrorWithKey(key: string): string {
return `${t(key + ".description")} ${t(key + ".details")}`
}
return {
translateError,
translateErrorWithKey
}
}

View File

@@ -28,6 +28,10 @@ export class Me {
hasAnyRole() { hasAnyRole() {
return true; return true;
} }
getNamespacesForAction(_permission: any, _action: any): string[] {
return [];
}
} }
export const useAuthStore = defineStore("auth", { export const useAuthStore = defineStore("auth", {

View File

@@ -195,7 +195,7 @@ export const useFlowStore = defineStore("flow", () => {
return validateFlow({ return validateFlow({
flow: (isCreating.value ? flowYaml.value : yamlWithNextRevision.value) ?? "" flow: (isCreating.value ? flowYaml.value : yamlWithNextRevision.value) ?? ""
}) })
.then((value: {constraints?: any}) => { .then((value: {constraints?: string}) => {
if ( if (
topologyVisible && topologyVisible &&
flowHaveTasks.value && flowHaveTasks.value &&
@@ -566,7 +566,7 @@ function deleteFlowAndDependencies() {
coreStore.message = { coreStore.message = {
title: "Couldn't expand subflow", title: "Couldn't expand subflow",
message: error.response.data.message, message: error.response.data.message,
variant: "danger" variant: "error"
}; };
} }
@@ -644,19 +644,37 @@ function deleteFlowAndDependencies() {
function enableFlowByQuery(options: { namespace: string, id: string }) { function enableFlowByQuery(options: { namespace: string, id: string }) {
return axios.post(`${apiUrl()}/flows/enable/by-query`, options, {params: options}) return axios.post(`${apiUrl()}/flows/enable/by-query`, options, {params: options})
} }
function deleteFlowByIds(options: { ids: {id: string, namespace: string}[] }) { function deleteFlowByIds(options: { ids: {id: string, namespace: string}[] }) {
return axios.delete(`${apiUrl()}/flows/delete/by-ids`, {data: options.ids}) return axios.delete(`${apiUrl()}/flows/delete/by-ids`, {data: options.ids})
} }
function deleteFlowByQuery(options: { namespace: string, id: string }) { function deleteFlowByQuery(options: { namespace: string, id: string }) {
return axios.delete(`${apiUrl()}/flows/delete/by-query`, {params: options}) return axios.delete(`${apiUrl()}/flows/delete/by-query`, {params: options})
} }
function validateFlow(options: { flow: string }) { function validateFlow(options: { flow: string }) {
const flowValidationIssues: FlowValidations = {};
if(isCreating.value) {
const {namespace} = YAML_UTILS.getMetadata(options.flow);
if(authStore.user && !authStore.user.isAllowed(
permission.FLOW,
action.CREATE,
namespace,
)) {
flowValidationIssues.constraints = t("flow creation denied in namespace", {namespace});
}
}
return axios.post(`${apiUrl()}/flows/validate`, options.flow, {...textYamlHeader, withCredentials: true}) return axios.post(`${apiUrl()}/flows/validate`, options.flow, {...textYamlHeader, withCredentials: true})
.then(response => { .then(response => {
flowValidation.value = response.data[0] const constraintsArray = [response?.data[0]?.constraints, flowValidationIssues.constraints].filter(Boolean)
return response.data[0] flowValidation.value = constraintsArray.length === 0 ? {} : {
constraints: constraintsArray.join(", ")
};
return flowValidation.value
}) })
} }
function validateTask(options: { task: string, section: string }) { function validateTask(options: { task: string, section: string }) {
return axios.post(`${apiUrl()}/flows/validate/task`, options.task, {...textYamlHeader, withCredentials: true, params: {section: options.section}}) return axios.post(`${apiUrl()}/flows/validate/task`, options.task, {...textYamlHeader, withCredentials: true, params: {section: options.section}})
.then(response => { .then(response => {
@@ -752,7 +770,8 @@ function deleteFlowAndDependencies() {
return false; return false;
} }
return authStore.user.isAllowed( return (isCreating.value && authStore.user.hasAnyAction(permission.FLOW, action.UPDATE))
|| authStore.user.isAllowed(
permission.FLOW, permission.FLOW,
action.UPDATE, action.UPDATE,
flow.value?.namespace, flow.value?.namespace,
@@ -777,9 +796,10 @@ function deleteFlowAndDependencies() {
}) })
const flowErrors = computed((): string[] | undefined => { const flowErrors = computed((): string[] | undefined => {
const key = baseOutdatedTranslationKey.value;
const flowExistsError = const flowExistsError =
flowValidation.value?.outdated && isCreating.value flowValidation.value?.outdated && isCreating.value
? [`>>>>${baseOutdatedTranslationKey.value}`] // because translating is impossible here ? [`${t(key + ".description")} ${t(key + ".details")}`]
: []; : [];
const constraintsError = const constraintsError =
@@ -794,8 +814,6 @@ function deleteFlowAndDependencies() {
const infos = flowValidation.value?.infos ?? []; const infos = flowValidation.value?.infos ?? [];
return infos.length === 0 ? undefined : infos; return infos.length === 0 ? undefined : infos;
return undefined;
}) })
const flowHaveTasks = computed((): boolean => { const flowHaveTasks = computed((): boolean => {

View File

@@ -574,6 +574,7 @@
"can not save": "Can not save", "can not save": "Can not save",
"flow must not be empty": "Flow must not be empty", "flow must not be empty": "Flow must not be empty",
"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.",
"flow creation denied in namespace": "You don't have permission to create flows in the namespace `{namespace}`.",
"readonly property": "Read-only property", "readonly property": "Read-only property",
"namespace and id readonly": "The properties `namespace` and `id` cannot be changed — they are now set to their initial values. If you want to rename a flow or change its namespace, you can create a new flow and remove the old one.", "namespace and id readonly": "The properties `namespace` and `id` cannot be changed — they are now set to their initial values. If you want to rename a flow or change its namespace, you can create a new flow and remove the old one.",
"avg": "Average", "avg": "Average",