mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(blueprints): impl templated flow blueprints
# Conflicts: # core/src/main/java/io/kestra/core/serializers/YamlParser.java
This commit is contained in:
@@ -8,6 +8,7 @@ import io.micronaut.core.annotation.Nullable;
|
||||
import io.pebbletemplates.pebble.PebbleEngine;
|
||||
import io.pebbletemplates.pebble.extension.Extension;
|
||||
import io.pebbletemplates.pebble.extension.Function;
|
||||
import io.pebbletemplates.pebble.lexer.Syntax;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -37,6 +38,13 @@ public class PebbleEngineFactory {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public PebbleEngine createWithCustomSyntax(Syntax syntax, Class<? extends Extension> extension) {
|
||||
PebbleEngine.Builder builder = newPebbleEngineBuilder()
|
||||
.syntax(syntax);
|
||||
this.applicationContext.getBeansOfType(extension).forEach(builder::extension);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public PebbleEngine createWithMaskedFunctions(VariableRenderer renderer, final List<String> functionsToMask) {
|
||||
|
||||
PebbleEngine.Builder builder = newPebbleEngineBuilder();
|
||||
|
||||
@@ -35,6 +35,10 @@ public final class YamlParser {
|
||||
return read(input, cls, type(cls));
|
||||
}
|
||||
|
||||
public static <T> T parse(String input, Class<T> cls, Boolean strict) {
|
||||
return strict ? read(input, cls, type(cls)) : readNonStrict(input, cls, type(cls));
|
||||
}
|
||||
|
||||
public static <T> T parse(Map<String, Object> input, Class<T> cls, Boolean strict) {
|
||||
ObjectMapper currentMapper = strict ? STRICT_MAPPER : NON_STRICT_MAPPER;
|
||||
|
||||
@@ -81,6 +85,13 @@ public final class YamlParser {
|
||||
throw toConstraintViolationException(input, resource, e);
|
||||
}
|
||||
}
|
||||
private static <T> T readNonStrict(String input, Class<T> objectClass, String resource) {
|
||||
try {
|
||||
return NON_STRICT_MAPPER.readValue(input, objectClass);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw toConstraintViolationException(input, resource, e);
|
||||
}
|
||||
}
|
||||
private static String formatYamlErrorMessage(String originalMessage, JsonProcessingException e) {
|
||||
StringBuilder friendlyMessage = new StringBuilder();
|
||||
if (originalMessage.contains("Expected a field name")) {
|
||||
|
||||
46
ui/package-lock.json
generated
46
ui/package-lock.json
generated
@@ -4220,18 +4220,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.19.0.tgz",
|
||||
"integrity": "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz",
|
||||
"integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.19.0"
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs/node_modules/@shikijs/types": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz",
|
||||
"integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
"integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
@@ -4258,18 +4258,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.19.0.tgz",
|
||||
"integrity": "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz",
|
||||
"integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.19.0"
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes/node_modules/@shikijs/types": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.19.0.tgz",
|
||||
"integrity": "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ==",
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
"integrity": "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
@@ -18914,24 +18914,6 @@
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/langs": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.20.0.tgz",
|
||||
"integrity": "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/themes": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.20.0.tgz",
|
||||
"integrity": "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki/node_modules/@shikijs/types": {
|
||||
"version": "3.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.20.0.tgz",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
import {ref, computed, watch, onMounted, nextTick, useAttrs} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import EnterpriseBadge from "./EnterpriseBadge.vue";
|
||||
import BlueprintDetail from "./flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintDetail from "../override/components/flows/blueprints/BlueprintDetail.vue";
|
||||
|
||||
interface Tab {
|
||||
name?: string;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
const setupFlow = async () => {
|
||||
const blueprintId = route.query.blueprintId as string;
|
||||
const blueprintSource = route.query.blueprintSource as BlueprintType;
|
||||
const blueprintSourceYaml = route.query.blueprintSourceYaml as string;
|
||||
const implicitDefaultNamespace = authStore.user.getNamespacesForAction(
|
||||
permission.FLOW,
|
||||
action.CREATE,
|
||||
@@ -50,12 +51,17 @@
|
||||
|
||||
if (route.query.copy && flowStore.flow) {
|
||||
flowYaml = flowStore.flow.source;
|
||||
} else if (blueprintId && blueprintSource) {
|
||||
} else if (blueprintId && blueprintSourceYaml) {
|
||||
flowYaml = blueprintSourceYaml;
|
||||
} else if(blueprintId && blueprintSource === "community"){
|
||||
flowYaml = await blueprintsStore.getBlueprintSource({
|
||||
type: blueprintSource,
|
||||
kind: "flow",
|
||||
id: blueprintId
|
||||
});
|
||||
} else if (blueprintId) {
|
||||
const flowBlueprint = await blueprintsStore.getFlowBlueprint(blueprintId);
|
||||
flowYaml = flowBlueprint.source;
|
||||
} else {
|
||||
flowYaml = `
|
||||
id: ${id}
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
<script lang="ts">
|
||||
import {ElMessage} from "element-plus";
|
||||
import ValidationError from "../flows/ValidationError.vue";
|
||||
import {toRaw} from "vue";
|
||||
import {markRaw, toRaw} from "vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import debounce from "lodash/debounce";
|
||||
@@ -336,10 +336,10 @@
|
||||
editingArrayId: null,
|
||||
editableItems: {},
|
||||
// expose icon components to the template so linters and the template can resolve them
|
||||
DeleteOutline,
|
||||
Pencil,
|
||||
Plus,
|
||||
ContentSave
|
||||
DeleteOutline: markRaw(DeleteOutline),
|
||||
Pencil:markRaw(Pencil),
|
||||
Plus:markRaw(Plus),
|
||||
ContentSave:markRaw(ContentSave)
|
||||
};
|
||||
},
|
||||
emits: ["update:modelValue", "update:modelValueNoDefault", "update:checks", "confirm", "validation"],
|
||||
@@ -566,6 +566,7 @@
|
||||
} else {
|
||||
this.$emit("validation", {
|
||||
formData: formData,
|
||||
inputsMetaData: this.inputsMetaData,
|
||||
callback: (response) => {
|
||||
metadataCallback(response);
|
||||
}
|
||||
|
||||
@@ -90,16 +90,16 @@
|
||||
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
|
||||
import Editor from "../../inputs/Editor.vue";
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import TopNavBar from "../../layout/TopNavBar.vue";
|
||||
import LowCodeEditor from "../../inputs/LowCodeEditor.vue";
|
||||
import CopyToClipboard from "../../layout/CopyToClipboard.vue";
|
||||
import Editor from "../../../../components/inputs/Editor.vue";
|
||||
import Markdown from "../../../../components/layout/Markdown.vue";
|
||||
import TopNavBar from "../../../../components/layout/TopNavBar.vue";
|
||||
import LowCodeEditor from "../../../../components/inputs/LowCodeEditor.vue";
|
||||
import CopyToClipboard from "../../../../components/layout/CopyToClipboard.vue";
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import {usePluginsStore} from "../../../stores/plugins";
|
||||
import {useBlueprintsStore} from "../../../stores/blueprints";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
import {usePluginsStore} from "../../../../stores/plugins";
|
||||
import {useBlueprintsStore} from "../../../../stores/blueprints";
|
||||
|
||||
import {canCreate} from "override/composables/blueprintsPermissions";
|
||||
import {parse as parseFlow} from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
@@ -35,9 +35,8 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import TopNavBar from "../../../../components/layout/TopNavBar.vue";
|
||||
import DottedLayout from "../../../../components/layout/DottedLayout.vue";
|
||||
// @ts-expect-error - Component not typed
|
||||
import BlueprintDetail from "../../../../components/flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintsBrowser from "./BlueprintsBrowser.vue";
|
||||
import BlueprintDetail from "../../../../override/components/flows/blueprints/BlueprintDetail.vue";
|
||||
import BlueprintsBrowser from "../../../../override/components/flows/blueprints/BlueprintsBrowser.vue";
|
||||
import DemoBlueprints from "../../../../components/demo/Blueprints.vue";
|
||||
import useRouteContext from "../../../../composables/useRouteContext";
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
</div>
|
||||
|
||||
<div class="action-button">
|
||||
<slot name="buttons" :blueprint="blueprint" />
|
||||
<el-tooltip v-if="embed && !system" trigger="click" content="Copied" placement="left" :autoClose="2000" effect="light">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -77,9 +76,11 @@
|
||||
class="p-2"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-button v-else-if="userCanCreate" type="primary" size="default" @click.prevent.stop="blueprintToEditor(blueprint.id)">
|
||||
<slot name="buttons" :blueprint="{...blueprint, kind: props.blueprintKind, type: props.blueprintType}">
|
||||
<el-button v-if="!embed && userCanCreate" type="primary" size="default" @click.prevent.stop="blueprintToEditor(blueprint.id)">
|
||||
{{ $t('use') }}
|
||||
</el-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default [
|
||||
|
||||
//Blueprints
|
||||
{name: "blueprints", path: "/:tenant?/blueprints/:kind/:tab", component: () => import("override/components/flows/blueprints/Blueprints.vue"), props: true},
|
||||
{name: "blueprints/view", path: "/:tenant?/blueprints/:kind/:tab/:blueprintId", component: () => import("../components/flows/blueprints/BlueprintDetail.vue"), props: true},
|
||||
{name: "blueprints/view", path: "/:tenant?/blueprints/:kind/:tab/:blueprintId", component: () => import("../override/components/flows/blueprints/BlueprintDetail.vue"), props: true},
|
||||
|
||||
//Documentation
|
||||
{name: "plugins/list", path: "/:tenant?/plugins", component: () => import("../components/plugins/Plugin.vue")},
|
||||
|
||||
@@ -24,6 +24,31 @@ interface Blueprint {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TemplateArgument {
|
||||
id: string,
|
||||
displayName: string,
|
||||
type: string,
|
||||
itemType?: string,
|
||||
required: boolean,
|
||||
defaults?: any
|
||||
}
|
||||
|
||||
export interface BlueprintTemplate {
|
||||
source: string;
|
||||
templateArguments: Record<string, TemplateArgument>;
|
||||
}
|
||||
|
||||
export interface FlowBlueprint {
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
includedTasks?: string[],
|
||||
tags?: string[],
|
||||
source: string,
|
||||
publishedAt?: string,
|
||||
template?: BlueprintTemplate
|
||||
}
|
||||
|
||||
const API_URL = "https://api.kestra.io/v1";
|
||||
const VALIDATE = {validateStatus: (status: number) => status === 200 || status === 401};
|
||||
|
||||
@@ -95,6 +120,54 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const getFlowBlueprint = async (id: string): Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flow/${id}`;
|
||||
|
||||
const response = await axios.get(url);
|
||||
|
||||
if (response.data?.id) {
|
||||
trackBlueprintSelection(response.data.id);
|
||||
}
|
||||
|
||||
blueprint.value = response.data;
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const createFlowBlueprint = async (toCreate: {source: string, title: string, description: string, tags: string[]}): Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flows`;
|
||||
const body = {
|
||||
...toCreate
|
||||
}
|
||||
const response = await axios.post(url, body);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const updateFlowBlueprint = async (id: string, toUpdate: {source: string, title: string, description: string, tags: string[]}) :Promise<FlowBlueprint> => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${id}`;
|
||||
const body = {
|
||||
...toUpdate
|
||||
}
|
||||
const response = await axios.put(url, body);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
const deleteFlowBlueprint = async (idToDelete: string) => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${idToDelete}`;
|
||||
await axios.delete(url);
|
||||
};
|
||||
|
||||
const useFlowBlueprintTemplate = async (id: string, inputs: Record<string, object>): Promise<{generatedFlowSource: string}> => {
|
||||
const url = `${apiUrl()}/blueprints/flows/${id}/use-template`;
|
||||
const body = {
|
||||
templateArgumentsInputs: inputs
|
||||
}
|
||||
const response = await axios.post(url, body);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return {
|
||||
blueprint,
|
||||
blueprints,
|
||||
@@ -106,5 +179,10 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
|
||||
getBlueprintSource,
|
||||
getBlueprintGraph,
|
||||
getBlueprintTags,
|
||||
useFlowBlueprintTemplate,
|
||||
getFlowBlueprint,
|
||||
createFlowBlueprint,
|
||||
updateFlowBlueprint,
|
||||
deleteFlowBlueprint,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user