Compare commits

..

17 Commits

Author SHA1 Message Date
Bart Ledoux
349c5ad4ad still wip but better 2025-12-23 13:25:01 +01:00
Bart Ledoux
219d68a092 Merge branch 'develop' into spike/generate-axios-api 2025-12-23 12:13:11 +01:00
Bart Ledoux
2e11a6c851 npm audit fix 2025-12-23 12:11:00 +01:00
Bart Ledoux
aadd0877d5 add more typings 2025-12-18 13:44:49 +01:00
Bart Ledoux
ec19287685 small adjustments 2025-12-18 13:43:54 +01:00
Bart Ledoux
1a9bdf6caa update the API and use it in the flows 2025-12-18 00:12:39 +01:00
Bart Ledoux
dbeface7c6 remove function useSdk 2025-12-17 22:40:54 +01:00
Bart Ledoux
ea9a86545c set a main tenant in a plugin 2025-12-17 22:37:23 +01:00
Bart Ledoux
8b5af1f8a3 generate flat sdk to allow treeshaking 2025-12-16 17:31:01 +01:00
Bart Ledoux
85adf521be make sure the app can start 2025-12-16 17:26:24 +01:00
Bart Ledoux
5dd0ad6036 add generated to gitignore 2025-12-16 17:25:52 +01:00
Bart Ledoux
33abe9980e remove generated files 2025-12-16 17:00:39 +01:00
Bart Ledoux
2a4097fbc9 Merge branch 'develop' into spike/generate-axios-api 2025-12-16 16:59:23 +01:00
Bart Ledoux
059262514c try and use sdk in flow 2025-12-15 16:24:49 +01:00
Bart Ledoux
c6b7021a0b first use of the sdk 2025-12-15 15:56:29 +01:00
Bart Ledoux
1548e31182 make the sdk tenant ready 2025-12-15 15:56:12 +01:00
Bart Ledoux
aae8011221 first hey sdk 2025-12-15 15:35:45 +01:00
51 changed files with 2215 additions and 1973 deletions

View File

@@ -63,9 +63,9 @@ You can also build it from a terminal using `./gradlew build`, the Gradle wrappe
- Configure the following environment variables:
- `MICRONAUT_ENVIRONMENTS`: can be set to any string and will load a custom configuration file in `cli/src/main/resources/application-{env}.yml`.
- `KESTRA_PLUGINS_PATH`: is the path where you will save plugins as Jar and will be load on startup.
- See the screenshot below for an example: ![Intellij IDEA Configuration ](./assets/run-app.png)
- See the screenshot below for an example: ![Intellij IDEA Configuration ](run-app.png)
- If you encounter **JavaScript memory heap out** error during startup, configure `NODE_OPTIONS` environment variable with some large value.
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](./assets/node_option_env_var.png)
- Example `NODE_OPTIONS: --max-old-space-size=4096` or `NODE_OPTIONS: --max-old-space-size=8192` ![Intellij IDEA Configuration ](node_option_env_var.png)
- The server starts by default on port 8080 and is reachable on `http://localhost:8080`
If you want to launch all tests, you need Python and some packages installed on your machine, on Ubuntu you can install them with:

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -222,7 +222,6 @@ subprojects {subProj ->
def commonTestConfig = { Test t ->
t.ignoreFailures = true
t.finalizedBy jacocoTestReport
// set Xmx for test workers
t.maxHeapSize = '4g'

View File

@@ -4,6 +4,7 @@ import io.micronaut.configuration.picocli.PicocliRunner;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -14,6 +15,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,8 +25,7 @@ class PluginDocCommandTest {
@Test
void run() throws IOException, URISyntaxException {
var testDirectoryName = PluginListCommandTest.class.getSimpleName();
Path pluginsPath = Files.createTempDirectory(testDirectoryName + "_pluginsPath_");
Path pluginsPath = Files.createTempDirectory(PluginListCommandTest.class.getSimpleName());
pluginsPath.toFile().deleteOnExit();
FileUtils.copyFile(
@@ -33,7 +34,7 @@ class PluginDocCommandTest {
new File(URI.create("file://" + pluginsPath.toAbsolutePath() + "/" + PLUGIN_TEMPLATE_TEST))
);
Path docPath = Files.createTempDirectory(testDirectoryName + "_docPath_");
Path docPath = Files.createTempDirectory(PluginInstallCommandTest.class.getSimpleName());
docPath.toFile().deleteOnExit();
try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
@@ -42,9 +43,9 @@ class PluginDocCommandTest {
List<Path> files = Files.list(docPath).toList();
assertThat(files.stream().map(path -> path.getFileName().toString())).contains("plugin-template-test");
// don't know why, but sometimes there is an addition "plugin-notifications" directory present
var directory = files.stream().filter(path -> "plugin-template-test".equals(path.getFileName().toString())).findFirst().get().toFile();
assertThat(files.size()).isEqualTo(1);
assertThat(files.getFirst().getFileName().toString()).isEqualTo("plugin-template-test");
var directory = files.getFirst().toFile();
assertThat(directory.isDirectory()).isTrue();
assertThat(directory.listFiles().length).isEqualTo(3);

View File

@@ -8,31 +8,25 @@ import io.micronaut.test.extensions.junit5.MicronautJunit5Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import java.util.Set;
public class KestraTestExtension extends MicronautJunit5Extension {
@Override
protected MicronautTestValue buildMicronautTestValue(Class<?> testClass) {
testProperties.put("kestra.jdbc.executor.thread-count", Runtime.getRuntime().availableProcessors() * 4);
return AnnotationSupport
.findAnnotation(testClass, KestraTest.class)
.map(kestraTestAnnotation -> {
var envsSet = new java.util.HashSet<>(Set.of(kestraTestAnnotation.environments()));
envsSet.add("test");// add test env if not already present
return new MicronautTestValue(
kestraTestAnnotation.application(),
envsSet.toArray(new String[0]),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
);
})
.map(kestraTestAnnotation -> new MicronautTestValue(
kestraTestAnnotation.application(),
kestraTestAnnotation.environments(),
kestraTestAnnotation.packages(),
kestraTestAnnotation.propertySources(),
kestraTestAnnotation.rollback(),
kestraTestAnnotation.transactional(),
kestraTestAnnotation.rebuildContext(),
kestraTestAnnotation.contextBuilder(),
kestraTestAnnotation.transactionMode(),
kestraTestAnnotation.startApplication(),
kestraTestAnnotation.resolveParameters()
))
.orElse(null);
}

3
ui/.gitignore vendored
View File

@@ -3,4 +3,5 @@ test-results
tests/.env
tests/data/
tests/e2e/.env
tests/e2e/data/application-secrets.yml
tests/e2e/data/application-secrets.yml
generated/

View File

@@ -23,6 +23,13 @@ export default defineConfig([
],
languageOptions: {globals: globals.node},
},
{
files: ["src/generated/**/*.ts"],
rules: {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
...pluginVue.configs["flat/strongly-recommended"],
{
files: ["**/*.vue", "**/*.tsx", "**/*.jsx"],

View File

@@ -0,0 +1,20 @@
import {definePluginConfig} from "@hey-api/openapi-ts";
import {handler} from "./plugin";
import type {KestraSdkPlugin} from "./types";
const defaultConfig: KestraSdkPlugin["Config"] = {
config: {
output: "kestra-sdk",
methodNameBuilder(operation) {
return operation.operationId
}
},
dependencies: ["@hey-api/typescript", "@hey-api/client-axios", "@hey-api/sdk"],
handler,
name: "ks-sdk",
};
/**
* Type helper for `@kestra-io/sdk-plugin` plugin, returns {@link Plugin.Config} object
*/
export const defineKestraHeyConfig = definePluginConfig(defaultConfig);

View File

@@ -0,0 +1,2 @@
export {defineKestraHeyConfig} from "./config";
export type {KestraSdkPlugin} from "./types";

View File

@@ -0,0 +1,169 @@
import {$} from "@hey-api/openapi-ts";
import type {KestraSdkPlugin} from "./types";
export const handler: KestraSdkPlugin["Handler"] = ({plugin}) => {
const useRouteSymbol = plugin.symbol(
"useRoute",
{
external: "vue-router"
});
const addTenantToParametersSymbol = plugin.symbol("addTenantToParameters",{
getFilePath: () => "sdk/ks-shared",
});
const functionNode = $.func().generic("TParams")
.params(
$.param("parameters").type($.type("TParams"))
).returns($.type.and($.type("TParams"), $.type.object().prop("tenant", (p) => p.type("string"))))
.do(
// const tenant = useRouter().params.tenant
$.const("tenant").assign(
$(useRouteSymbol).call().attr("params").attr("tenant").optional().as($.type("string"))
),
$.return($.object()
.spread($.id("parameters"))
.prop("tenant", "tenant")
)
)
const exportedFunctionNode = $.const(addTenantToParametersSymbol).export().assign(functionNode);
plugin.node(exportedFunctionNode);
const operationsDict: Record<string, {symbol:ReturnType<typeof plugin.symbol>, methodName: string}[]> = {}
plugin.forEach(
"operation",
({operation}) => {
// on each operation, create a method that executes the operation from the sdk
const methodName = plugin.config.methodNameBuilder?.(operation);
if (!methodName) {
return;
}
const pathParams = operation.parameters?.path || {};
const sym = plugin.querySymbol({
category: "sdk",
resource: "operation",
resourceId: operation.id,
})
if(!sym) {
return;
}
const originalOperationSymbol = $(sym);
const funcSymbol = plugin.symbol(methodName, {
getFilePath: () => `sdk/ks-${operation.tags?.[0] ?? "default"}`,
})
if (!operationsDict[operation.tags?.[0] ?? "default"]) {
operationsDict[operation.tags?.[0] ?? "default"] = [];
}
operationsDict[operation.tags?.[0] ?? "default"].push({symbol:funcSymbol, methodName});
if(!pathParams || !("tenant" in pathParams)) {
// if there is no path parameter named "tenant",
// we export this method as is
plugin.node(
$.const(funcSymbol)
.assign(originalOperationSymbol)
.export()
);
return;
}
const optionsId = "options"
// find a cleaner way to do that (expose parameters symbol from operation ?)
const parametersWithoutTenant = sym.node?.value._params[0]._type._exprInput["~ref"].props.filter((p: any) => p.name !== "tenant") as any
if(parametersWithoutTenant.length === 0) {
// if the only path parameter is "tenant", we can simplify the function
const functionNode = $.func()
.params($.param(optionsId)
.required(false)
.type(
$.type("Parameters")
.generic($.type.query(originalOperationSymbol))
.idx(1)
)
)
.do(
$.return(originalOperationSymbol.call(
$(addTenantToParametersSymbol).call($.object()),
optionsId,
))
)
const exportedFunctionNode = $.const(funcSymbol).export().assign(functionNode);
plugin.node(exportedFunctionNode);
return;
}
const isTenantOnlyRequiredParam = Object.values(pathParams).filter(p => p.name !== "tenant" && p.required).length === 0;
const parameterObj = $.type.object()
for (const param in parametersWithoutTenant) {
const paramDef = parametersWithoutTenant[param];
parameterObj.prop(paramDef.name, (p) => p.required(!paramDef._optional).type(paramDef._type["~ref"]));
}
const paramId = "parameters"
const functionNode = $.func()
.params(
$.param(paramId)
.required(!isTenantOnlyRequiredParam)
.type(parameterObj)
,
$.param(optionsId)
.required(false)
.type(
$.type("Parameters")
.generic($.type.query(originalOperationSymbol))
.idx(1)
)
)
.do(
isTenantOnlyRequiredParam ?
$.return(originalOperationSymbol.call(
$(addTenantToParametersSymbol).call($(paramId)),
optionsId,
))
: $.return(originalOperationSymbol.call(
$(addTenantToParametersSymbol).call(paramId),
optionsId,
))
)
const exportedFunctionNode = $.const(funcSymbol).export().assign(functionNode);
plugin.node(exportedFunctionNode);
},
{
order: "declarations",
},
);
for (const tag in operationsDict) {
const operations = operationsDict[tag];
const symbol = plugin.symbol(tag, {
getFilePath: () => "ks-sdk",
});
plugin.node(
$.const(symbol)
.export()
.assign($.object().props(...operations.map(op => $.prop({
kind: "prop",
name: op.methodName
}).value(op.symbol))))
);
}
};

21
ui/heyapi-sdk-plugin/types.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
import type {DefinePlugin} from "@hey-api/openapi-ts";
export type UserConfig = {
/**
* Plugin name. Must be unique.
*/
name: "ks-sdk";
/**
* Name of the generated file.
*
* @default 'ks-sdk'
*/
output?: string;
/**
* Function to build method names from operations.
* Receives the operation object and must return a string or undefined to skip the operation.
*/
methodNameBuilder?: (operation: any) => string;
};
export type KestraSdkPlugin = DefinePlugin<UserConfig>;

35
ui/openapi-ts.config.ts Normal file
View File

@@ -0,0 +1,35 @@
import {defineConfig} from "@hey-api/openapi-ts";
import {defineKestraHeyConfig} from "./heyapi-sdk-plugin";
const generateHash = (str: string) => {
let hash = 0;
for (const char of str) {
hash = (hash << 5) - hash + char.charCodeAt(0);
hash |= 0; // Constrain to 32bit integer
}
return hash.toString(16).replace("-", "0");
};
export default defineConfig({
input: "../webserver/build/classes/java/main/META-INF/swagger/kestra.yml",
output: {
path: "./src/generated/kestra-api",
lint: "eslint"
},
plugins: [
{
name: "@hey-api/client-axios",
},
{
name: "@hey-api/sdk",
paramsStructure: "flat",
methodNameBuilder(operation) {
return `__${generateHash(operation.id)}__`
}
},
defineKestraHeyConfig({
output: "./src/generated/kestra-heyapi-sdk",
})
],
});

3403
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,14 +20,15 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"prepare": "cd .. && husky ui/.husky && rimraf .git/hooks",
"postinstall": "patch-package"
"generate:openapi": "openapi-ts",
"postinstall": "patch-package && openapi-ts"
},
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.268",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.48.1",
"@vue-flow/core": "^1.48.0",
"@vueuse/core": "^14.1.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.13.2",
@@ -39,7 +40,7 @@
"cytoscape": "^3.33.0",
"dagre": "^0.8.5",
"dotenv": "^17.2.3",
"element-plus": "2.13.0",
"element-plus": "2.12.0",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
@@ -59,15 +60,15 @@
"path-browserify": "^1.0.1",
"pdfjs-dist": "^5.4.449",
"pinia": "^3.0.4",
"posthog-js": "^1.310.1",
"posthog-js": "^1.308.0",
"rapidoc": "^9.3.8",
"semver": "^7.7.3",
"shiki": "^3.20.0",
"vue": "^3.5.26",
"vue": "^3.5.25",
"vue-axios": "^3.5.2",
"vue-chartjs": "^5.3.3",
"vue-gtag": "^3.6.3",
"vue-i18n": "^11.2.7",
"vue-i18n": "^11.2.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^4.6.4",
"vue-sidebar-menu": "^5.9.1",
@@ -81,6 +82,7 @@
"@codecov/vite-plugin": "^1.9.1",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@eslint/js": "^9.39.2",
"@hey-api/openapi-ts": "^0.89.2",
"@playwright/test": "^1.57.0",
"@rushstack/eslint-patch": "^1.14.1",
"@shikijs/markdown-it": "^3.20.0",
@@ -97,9 +99,9 @@
"@types/semver": "^7.7.1",
"@types/testing-library__jest-dom": "^6.0.0",
"@types/testing-library__user-event": "^4.2.0",
"@typescript-eslint/parser": "^8.50.1",
"@typescript-eslint/parser": "^8.50.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@vitejs/plugin-vue-jsx": "^5.1.2",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@vue/eslint-config-prettier": "^10.2.0",
@@ -120,29 +122,29 @@
"playwright": "^1.55.0",
"prettier": "^3.7.4",
"rimraf": "^6.1.2",
"rolldown-vite": "^7.3.0",
"rolldown-vite": "^7.2.11",
"rollup-plugin-copy": "^3.5.0",
"sass": "^1.97.1",
"storybook": "^9.1.17",
"sass": "^1.97.0",
"storybook": "^9.1.16",
"storybook-vue3-router": "^6.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.1",
"typescript-eslint": "^8.50.0",
"uuid": "^13.0.0",
"vite": "npm:rolldown-vite@latest",
"vitest": "^3.2.4",
"vue-tsc": "^3.2.1"
"vue-tsc": "^3.1.8"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.27.2",
"@esbuild/darwin-x64": "^0.27.2",
"@esbuild/linux-x64": "^0.27.2",
"@rollup/rollup-darwin-arm64": "^4.54.0",
"@rollup/rollup-darwin-x64": "^4.54.0",
"@rollup/rollup-linux-x64-gnu": "^4.54.0",
"@swc/core-darwin-arm64": "^1.15.7",
"@swc/core-darwin-x64": "^1.15.7",
"@swc/core-linux-x64-gnu": "^1.15.7"
"@rollup/rollup-darwin-arm64": "^4.53.5",
"@rollup/rollup-darwin-x64": "^4.53.5",
"@rollup/rollup-linux-x64-gnu": "^4.53.5",
"@swc/core-darwin-arm64": "^1.15.5",
"@swc/core-darwin-x64": "^1.15.5",
"@swc/core-linux-x64-gnu": "^1.15.5"
},
"overrides": {
"bootstrap": {

View File

@@ -54,11 +54,12 @@
import {useI18n} from "vue-i18n";
import TopNavBar from "../layout/TopNavBar.vue";
import useRouteContext from "../../composables/useRouteContext";
import {useAxios} from "../../utils/axios";
import {useAxios, useSDK} from "../../utils/axios";
import IconEdit from "vue-material-design-icons/Pencil.vue";
import {apiUrl, apiUrlWithoutTenants} from "override/utils/route";
import {apiUrlWithoutTenants} from "override/utils/route";
import DataTable from "../layout/DataTable.vue";
import NoData from "../layout/NoData.vue";
import {PagedResultsConcurrencyLimit} from "../../generated/kestra-api";
const {t} = useI18n();
@@ -78,15 +79,13 @@
const KEYS: (keyof ConcurrencyLimit)[] = ["tenantId", "namespace", "flowId", "running"];
const axios = useAxios();
const data = ref<{
total: number;
results: ConcurrencyLimit[]
}>();
const sdk = useSDK();
const data = ref<PagedResultsConcurrencyLimit>();
async function loadData(){
const response = await axios.get(`${apiUrl()}/concurrency-limit/search`);
if(response?.status !== 200){
throw new Error(`Failed to load concurrency limits: ${response?.statusText}`);
const response = await sdk.Executions.searchConcurrencyLimits();
if(response?.status !== 200 && response?.error){
throw new Error(`Failed to load concurrency limits: ${response.error ?? "unknown error"}`);
}
data.value = response.data;
}

View File

@@ -43,7 +43,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY
UPDATE_TASK_FUNCTION_INJECTION_KEY
} from "../../no-code/injectionKeys";
import {NoCodeProps} from "../../flows/noCodeTypes";
import {deepEqual} from "../../../utils/utils";
@@ -68,7 +68,7 @@
dashboardStore.sourceCode = YAML_UTILS.stringify(app);
}
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -1,13 +1,16 @@
<template>
<el-tooltip placement="bottom" :content="$t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="$t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
<el-tooltip placement="bottom" :content="t('playground.tooltip_persistence')">
<el-switch v-model="playgroundStore.enabled" :activeText="t('playground.toggle')" class="toggle" :class="{'is-active': playgroundStore.enabled}" />
</el-tooltip>
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import {usePlaygroundStore} from "../../stores/playground";
const {t} = useI18n();
const playgroundStore = usePlaygroundStore();
</script>
<style scoped lang="scss">

View File

@@ -6,7 +6,7 @@
<Keyboard />
</el-icon>
<span class="fs-6">
{{ $t("editor_shortcuts.label") }}
{{ t("editor_shortcuts.label") }}
</span>
</div>
</template>
@@ -27,7 +27,7 @@
</template>
</div>
<div class="text-break">
{{ $t(command.description) }}
{{ t(command.description) }}
</div>
</div>
</div>
@@ -35,9 +35,11 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import Keyboard from "vue-material-design-icons/Keyboard.vue";
import {useKeyShortcuts} from "../../utils/useKeyShortcuts";
const {t} = useI18n();
const {isKeyShortcutsDialogShown} = useKeyShortcuts();
const commands = [

View File

@@ -97,6 +97,7 @@
<script setup lang="ts">
import {
computed,
getCurrentInstance,
h,
inject,
onBeforeUnmount,
@@ -125,7 +126,7 @@
import uniqBy from "lodash/uniqBy";
import {useI18n} from "vue-i18n";
import {ElDatePicker} from "element-plus";
import moment, {Moment} from "moment";
import {Moment} from "moment";
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget";
import Utils from "../../utils/utils";
import {hashCode} from "../../utils/global";
@@ -136,6 +137,7 @@
import EditorType = editor.EditorType;
import {useRoute} from "vue-router";
const currentInstance = getCurrentInstance()!;
const {t} = useI18n();
const textAreaValue = computed({
@@ -369,7 +371,8 @@
}
}, {deep: true});
const nowMoment: Moment = moment().startOf("day");
const nowMoment: Moment = currentInstance.appContext.config.globalProperties.$moment().startOf("day");
function addedSuggestRows(mutations: MutationRecord[]) {
return mutations.flatMap(({addedNodes}) => {
const nodes = [...addedNodes];
@@ -458,7 +461,7 @@
endColumn: wordAtPosition?.endColumn ?? position?.column
},
// We don't use the selectedDate directly because if user modifies the input value directly it doesn't work otherwise
text: `${moment(
text: `${currentInstance.appContext.config.globalProperties.$moment(
datePicker.value!.$el.nextElementSibling.querySelector("input").value
).toISOString(true)} `,
forceMoveMarkers: true

View File

@@ -1,11 +1,11 @@
<template>
<nav class="d-flex align-items-center w-100 gap-3 top-bar">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column flex-grow-1 flex-shrink-1 overflow-hidden top-title">
<div class="d-flex align-items-end gap-2">
<SidebarToggleButton
v-if="layoutStore.sideMenuCollapsed"
@toggle="layoutStore.setSideMenuCollapsed(false)"
/>
<div class="d-flex flex-column gap-2">
<el-breadcrumb v-if="breadcrumb">
<el-breadcrumb-item v-for="(item, x) in breadcrumb" :key="x" :class="{'pe-none': item.disabled}">

View File

@@ -43,9 +43,7 @@
BLOCK_SCHEMA_PATH_INJECTION_KEY,
CLOSE_TASK_FUNCTION_INJECTION_KEY,
CREATE_TASK_FUNCTION_INJECTION_KEY,
CREATING_FLOW_INJECTION_KEY,
CREATING_TASK_INJECTION_KEY,
DEFAULT_NAMESPACE_INJECTION_KEY,
EDIT_TASK_FUNCTION_INJECTION_KEY,
EDITING_TASK_INJECTION_KEY,
FIELDNAME_INJECTION_KEY,
@@ -57,7 +55,7 @@
REF_PATH_INJECTION_KEY,
ROOT_SCHEMA_INJECTION_KEY,
SCHEMA_DEFINITIONS_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
} from "./injectionKeys";
import {useFlowFields, SECTIONS_IDS} from "./utils/useFlowFields";
import debounce from "lodash/debounce";
@@ -67,7 +65,6 @@
import {useKeyboardSave} from "./utils/useKeyboardSave";
import {deepEqual} from "../../utils/utils";
import {useScrollMemory} from "../../composables/useScrollMemory";
import {defaultNamespace} from "../../composables/useNamespaces";
const props = defineProps<NoCodeProps>();
@@ -169,8 +166,6 @@
provide(REF_PATH_INJECTION_KEY, props.refPath);
provide(PANEL_INJECTION_KEY, panel)
provide(POSITION_INJECTION_KEY, props.position ?? "after");
provide(CREATING_FLOW_INJECTION_KEY, flowStore.isCreating ?? false);
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => flowStore.flow?.namespace ?? defaultNamespace() ?? "company.team"));
provide(CREATING_TASK_INJECTION_KEY, props.creatingTask);
provide(EDITING_TASK_INJECTION_KEY, props.editingTask);
provide(FIELDNAME_INJECTION_KEY, props.fieldName);
@@ -189,7 +184,7 @@
emit("closeTask")
})
provide(UPDATE_YAML_FUNCTION_INJECTION_KEY, (yaml) => {
provide(UPDATE_TASK_FUNCTION_INJECTION_KEY, (yaml) => {
editorUpdate(yaml)
})

View File

@@ -44,6 +44,7 @@
<script setup lang="ts">
import {computed, inject, ref} from "vue";
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
import {useFlowStore} from "../../../../stores/flow";
import Creation from "./taskList/buttons/Creation.vue";
import Element from "./taskList/Element.vue";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
@@ -52,7 +53,7 @@
import {
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY, UPDATE_YAML_FUNCTION_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
} from "../../injectionKeys";
import {SECTIONS_MAP} from "../../../../utils/constants";
import {getValueAtJsonPath} from "../../../../utils/utils";
@@ -82,6 +83,8 @@
inheritAttrs: false
});
const flowStore = useFlowStore();
interface Task {
id:string,
type:string
@@ -147,8 +150,6 @@
const movedIndex = ref(-1);
const updateYaml = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {});
const moveElement = (
items: Record<string, any>[] | undefined,
elementID: string,
@@ -170,7 +171,7 @@
movedIndex.value = -1;
}, 200);
updateYaml(
flowStore.flowYaml =
YAML_UTILS.swapBlocks({
source:flow.value,
section: SECTIONS_MAP[section.value.toLowerCase() as keyof typeof SECTIONS_MAP],
@@ -178,7 +179,6 @@
key2:items[newIndex][keyName],
keyName,
})
);
};
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));

View File

@@ -8,17 +8,18 @@
</template>
<script lang="ts" setup>
import {onMounted, inject, computed, provide} from "vue";
import {computed, onMounted} from "vue";
import {useFlowStore} from "../../../../stores/flow";
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
import {CREATING_FLOW_INJECTION_KEY, DEFAULT_NAMESPACE_INJECTION_KEY} from "../../injectionKeys";
const modelValue = defineModel<string>();
const isCreating = inject(CREATING_FLOW_INJECTION_KEY, false);
const defaultNamespace = inject(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => ""));
provide(DEFAULT_NAMESPACE_INJECTION_KEY, computed(() => modelValue.value || defaultNamespace.value));
const flowStore = useFlowStore();
const isCreating = computed(() => flowStore.isCreating);
onMounted(() => {
const flowNamespace = defaultNamespace.value;
const flowNamespace = flowStore.flow?.namespace;
if (!modelValue.value && flowNamespace) {
modelValue.value = flowNamespace;
}

View File

@@ -24,7 +24,6 @@ export const POSITION_INJECTION_KEY = Symbol("position-injection-key") as Inject
* NOTE: different from the `isCreating` flag coming from the store. `isCreating` refers to the Complete flow being in creation
*/
export const CREATING_TASK_INJECTION_KEY = Symbol("creating-injection-key") as InjectionKey<boolean>
export const CREATING_FLOW_INJECTION_KEY = Symbol("creating-flow-injection-key") as InjectionKey<boolean>
/**
* When creating anew task, allows to specify a field where the new task should be injected.
* @example
@@ -52,9 +51,9 @@ export const EDIT_TASK_FUNCTION_INJECTION_KEY = Symbol("edit-function-injection-
*/
export const CLOSE_TASK_FUNCTION_INJECTION_KEY = Symbol("close-function-injection-key") as InjectionKey<() => void>
/**
* Call this function to update the full Yaml content
* We call this function when a task is changed, as soon as the first click or type is done
*/
export const UPDATE_YAML_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
export const UPDATE_TASK_FUNCTION_INJECTION_KEY = Symbol("update-function-injection-key") as InjectionKey<(yaml: string) => void>
/**
* Set this to override the contents of the no-code editor with a component of your choice
* This is used to display the metadata edition inputs
@@ -93,6 +92,4 @@ export const SCHEMA_DEFINITIONS_INJECTION_KEY = Symbol("schema-definitions-injec
export const DATA_TYPES_MAP_INJECTION_KEY = Symbol("data-types-injection-key") as InjectionKey<ComputedRef<Record<string, string[] | undefined>>>
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;
export const DEFAULT_NAMESPACE_INJECTION_KEY = Symbol("default-namespace-injection-key") as InjectionKey<ComputedRef<string>>;
export const ON_TASK_EDITOR_CLICK_INJECTION_KEY = Symbol("on-task-editor-click-injection-key") as InjectionKey<(elt?: Partial<NoCodeElement>) => void>;

View File

@@ -16,7 +16,7 @@
import {PLUGIN_DEFAULTS_SECTION, SECTIONS_MAP} from "../../../utils/constants";
import {
CLOSE_TASK_FUNCTION_INJECTION_KEY,
UPDATE_YAML_FUNCTION_INJECTION_KEY,
UPDATE_TASK_FUNCTION_INJECTION_KEY,
FULL_SOURCE_INJECTION_KEY, CREATING_TASK_INJECTION_KEY,
PARENT_PATH_INJECTION_KEY, POSITION_INJECTION_KEY,
REF_PATH_INJECTION_KEY, EDIT_TASK_FUNCTION_INJECTION_KEY,
@@ -37,7 +37,7 @@
const fieldName = inject(FIELDNAME_INJECTION_KEY, undefined);
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""));
const updateTask = inject(UPDATE_YAML_FUNCTION_INJECTION_KEY, () => {})
const updateTask = inject(UPDATE_TASK_FUNCTION_INJECTION_KEY, () => {})
const closeTaskAddition = inject(
CLOSE_TASK_FUNCTION_INJECTION_KEY,

View File

@@ -3,7 +3,7 @@
<template #additional-right>
<ul>
<li>
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="secondary">
<el-button v-if="canCreate" tag="router-link" :to="{name: 'flows/create', query: {namespace: $route.query.namespace}}" :icon="Plus" type="primary">
{{ $t('create_flow') }}
</el-button>
</li>
@@ -30,7 +30,7 @@
<el-button
v-if="isOSS"
@click="startTour"
:icon="Compass"
:icon="Plus"
size="large"
type="primary"
class="px-3 p-4 section-1-link product-link"
@@ -39,7 +39,7 @@
</el-button>
<el-button
v-else
:icon="Compass"
:icon="Plus"
tag="router-link"
:to="{name: 'flows/create'}"
size="large"
@@ -74,7 +74,6 @@
import {useCoreStore} from "../../stores/core";
import {useI18n} from "vue-i18n";
import Plus from "vue-material-design-icons/Plus.vue";
import Compass from "vue-material-design-icons/Compass.vue";
import Play from "vue-material-design-icons/Play.vue";
import OnboardingBottom from "override/components/OnboardingBottom.vue";
import kestraWelcome from "../../assets/onboarding/kestra_welcome.svg";

View File

@@ -156,7 +156,7 @@
<el-form-item :label="$t('secret.key')" prop="key">
<el-input v-model="secret.key" :disabled="secret.update" required />
</el-form-item>
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value" required>
<el-form-item v-if="!secret.update" :label="$t('secret.name')" prop="value">
<MultilineSecret v-model="secret.value" :placeholder="secretModalTitle" />
</el-form-item>
<el-form-item v-if="secret.update" :label="$t('secret.name')" prop="value">

View File

@@ -1,4 +1,3 @@
import {computed, watch} from "vue";
import {useI18n} from "vue-i18n";
import {configureMonacoYaml} from "monaco-yaml";
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
@@ -22,7 +21,6 @@ import {
registerPebbleAutocompletion
} from "./pebbleLanguageConfigurator";
import {usePluginsStore} from "../../../stores/plugins";
import {useBlueprintsStore} from "../../../stores/blueprints";
import {languages} from "monaco-editor/esm/vs/editor/editor.api";
import CompletionItem = languages.CompletionItem;
@@ -36,14 +34,11 @@ export class YamlLanguageConfigurator extends AbstractLanguageConfigurator {
}
async configureLanguage(pluginsStore: ReturnType<typeof usePluginsStore>) {
const validateYAML = computed(() => useBlueprintsStore().validateYAML);
watch(validateYAML, (shouldValidate) => configureMonacoYaml(monaco, {validate: shouldValidate}));
configureMonacoYaml(monaco, {
enableSchemaRequest: true,
hover: localStorage.getItem("hoverTextEditor") === "true",
completion: true,
validate: validateYAML.value ?? true,
validate: true,
format: true,
schemas: yamlSchemas()
});

View File

@@ -1,11 +1,5 @@
<template>
<SideBar
v-if="menu"
:menu
:showLink
@menu-collapse="onCollapse"
:class="{overlay: verticalLayout}"
>
<SideBar v-if="menu" :menu :showLink="showLink" @menu-collapse="onCollapse">
<template #footer>
<Auth />
</template>
@@ -17,9 +11,6 @@
import SideBar from "../../components/layout/SideBar.vue";
import Auth from "../../override/components/auth/Auth.vue";
import {useBreakpoints, breakpointsElement} from "@vueuse/core";
const verticalLayout = useBreakpoints(breakpointsElement).smallerOrEqual("sm");
withDefaults(defineProps<{
showLink?: boolean
}>(), {

View File

@@ -56,10 +56,7 @@
<div v-if="!system && blueprint.tags?.length > 0" class="tags-section">
<span v-for="tag in processedTags(blueprint.tags)" :key="tag.original" class="tag-item">{{ tag.display }}</span>
</div>
<div v-if="blueprint.template" class="tags-section">
<span class="tag-item">{{ $t('template') }}</span>
</div>
<div class="text-section">
<div class="text-section">
<h3 class="title">
{{ blueprint.title ?? blueprint.id }}
</h3>
@@ -154,7 +151,6 @@
id: string;
tags: string[];
title?: string;
template?: Record<string, any>;
}[] | undefined>(undefined);
const error = ref(false);
const icon = {ContentCopy};

View File

@@ -1,14 +1,15 @@
import axios from "axios";
import {defineStore} from "pinia";
import {apiUrl} from "override/utils/route";
import {AI} from "../generated/kestra-api/ks-sdk.gen";
export const useAiStore = defineStore("ai", {
actions: {
async generateFlow({userPrompt, flowYaml, conversationId}: {userPrompt: string, flowYaml: string, conversationId: string}) {
const response = await axios.post(`${apiUrl()}/ai/generate/flow`, {
userPrompt,
flowYaml,
conversationId
const response = await AI.generateFlow({
flowGenerationPrompt:{
userPrompt,
flowYaml,
conversationId
}
});
return response.data;

View File

@@ -25,8 +25,6 @@ interface Blueprint {
[key: string]: any;
}
export type TemplateArgument = Record<string, Input>;
export interface BlueprintTemplate {
source: string;
templateArguments: Record<string, Input>;
@@ -57,8 +55,6 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
const source = ref<string | undefined>(undefined);
const graph = ref<any | undefined>(undefined);
const validateYAML = ref<boolean>(true); // Used to enable/disable YAML validation in Monaco editor, for the purpose of Templated Blueprints
const getBlueprints = async (options: Options) => {
const PARAMS = {params: options.params, ...VALIDATE};
@@ -170,8 +166,6 @@ export const useBlueprintsStore = defineStore("blueprints", () => {
source,
graph,
validateYAML,
getBlueprints,
getBlueprint,
getBlueprintSource,

View File

@@ -1,8 +1,7 @@
import {defineStore} from "pinia";
import {apiUrl} from "override/utils/route";
import {ref} from "vue";
import {useAxios} from "../utils/axios";
import {Message} from "../components/ErrorToast.vue";
import * as sdk from "../generated/kestra-api/ks-sdk.gen";
interface GuidedProperties {
tourStarted: boolean;
@@ -21,13 +20,13 @@ export const useCoreStore = defineStore("core", () => {
template: undefined,
})
const monacoYamlConfigured = ref(false)
const tutorialFlows = ref<any[]>([])
const axios = useAxios();
const tutorialFlows = ref<any[]>([]);
async function readTutorialFlows() {
const response = await axios.get(`${apiUrl()}/flows/tutorial`);
tutorialFlows.value = response.data;
const response = await sdk.Flows.listFlowsByNamespace({
namespace: "tutorials",
})
tutorialFlows.value = response.data ?? [];
return response.data;
}

View File

@@ -17,13 +17,15 @@ import {apiUrl} from "override/utils/route";
import Utils from "../utils/utils";
import type {Dashboard, Chart, Request, Parameters} from "../components/dashboard/composables/useDashboards";
import type {Chart, Request, Parameters} from "../components/dashboard/composables/useDashboards";
import {useAxios} from "../utils/axios";
import {removeRefPrefix, usePluginsStore} from "./plugins";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import _throttle from "lodash/throttle";
import {useCoreStore} from "./core";
import {useI18n} from "vue-i18n";
import {Dashboards} from "../generated/kestra-api/ks-sdk.gen";
import {Dashboard} from "../generated/kestra-api";
@@ -46,17 +48,25 @@ export const useDashboardStore = defineStore("dashboard", () => {
async function list(options: Record<string, any>) {
const {sort, ...params} = options;
const response = await axios.get(`${apiUrl()}/dashboards?size=100${sort ? `&sort=${sort}` : ""}`, {params});
const response = await Dashboards.searchDashboards({
size: 100,
sort,
page: 1,
...params
})
return response.data;
}
async function load(id: Dashboard["id"]) {
const response = await axios.get(`${apiUrl()}/dashboards/${id}`, {validateStatus});
let dashboardLoaded: Dashboard;
async function load(id: string) {
const response = await Dashboards.getDashboard({id}, {validateStatus});
let dashboardLoaded: Dashboard & {id: string};
if (response.status === 200) dashboardLoaded = response.data;
else dashboardLoaded = {title: "Default", id, charts: [], sourceCode: ""};
if (response.status === 200 && response.data) {
dashboardLoaded = {...response.data, id};
} else {
dashboardLoaded = {title: "Default", id, charts: [], sourceCode: ""};
}
dashboard.value = dashboardLoaded;
sourceCode.value = dashboardLoaded.sourceCode ?? ""
@@ -69,12 +79,12 @@ export const useDashboardStore = defineStore("dashboard", () => {
return response.data;
}
async function update({id, source}: {id: Dashboard["id"]; source: Dashboard["sourceCode"];}) {
async function update({id, source}: {id: string; source: Dashboard["sourceCode"];}) {
const response = await axios.put(`${apiUrl()}/dashboards/${id}`, source, header);
return response.data;
}
async function deleteDashboard(id: Dashboard["id"]) {
async function deleteDashboard(id: string) {
const response = await axios.delete(`${apiUrl()}/dashboards/${id}`);
return response.data;
}
@@ -84,7 +94,7 @@ export const useDashboardStore = defineStore("dashboard", () => {
return response.data;
}
async function generate(id: Dashboard["id"], chartId: Chart["id"], parameters: Parameters) {
async function generate(id: string, chartId: Chart["id"], parameters: Parameters) {
const response = await axios.post(`${apiUrl()}/dashboards/${id}/charts/${chartId}`, parameters, {validateStatus});
return response.data;
}

View File

@@ -10,13 +10,15 @@ import {useUnsavedChangesStore} from "./unsavedChanges";
import {defineStore} from "pinia";
import {FlowGraph} from "@kestra-io/ui-libs/vue-flow-utils";
import {makeToast} from "../utils/toast";
import {InputType} from "../utils/inputs";
import {globalI18n} from "../translations/i18n";
import {transformResponse} from "../components/dependencies/composables/useDependencies";
import {useAuthStore} from "override/stores/auth";
import {useRoute} from "vue-router";
import {useAxios} from "../utils/axios";
import {defaultNamespace} from "../composables/useNamespaces";
import {Flow, FlowWithSource} from "../generated/kestra-api";
import * as sdk from "../generated/kestra-api/ks-sdk.gen";
import {InputType} from "../utils/inputs";
const textYamlHeader = {
headers: {
@@ -54,26 +56,9 @@ interface FlowValidations {
deprecationPaths?: string[];
}
export interface Flow {
id: string;
namespace: string;
source: string;
revision?: number;
deleted?: boolean;
disabled?: boolean;
labels?: Record<string, string | boolean>;
triggers?: Trigger[];
inputs?: Input[];
errors?: { message: string; code?: string, id?: string }[];
concurrency?: {
limit: number;
behavior: string;
};
}
export const useFlowStore = defineStore("flow", () => {
const flows = ref<Flow[]>()
const flow = ref<Flow>()
const flow = ref<FlowWithSource>()
const task = ref<Task>()
const search = ref<any[]>()
const total = ref<number>(0)
@@ -254,7 +239,10 @@ export const useFlowStore = defineStore("flow", () => {
const isCreatingBackup = isCreating.value;
if (isCreating.value && !overrideFlow) {
await createFlow({flow: flowSource ?? ""})
.then((response: Flow) => {
.then((response) => {
if(!response){
return;
}
toast.saved(response.id);
isCreating.value = false;
});
@@ -291,7 +279,7 @@ export const useFlowStore = defineStore("flow", () => {
async function initYamlSource() {
if (!flow.value) return;
const {source} = flow.value;
const {source = ""} = flow.value;
flowYaml.value = source;
flowYamlOrigin.value = source;
if (flowHaveTasks.value) {
@@ -299,21 +287,22 @@ export const useFlowStore = defineStore("flow", () => {
}
// validate flow on first load
return validateFlow({flow: isCreating.value ? source : yamlWithNextRevision.value})
return validateFlow({
flow: isCreating.value ? source : yamlWithNextRevision.value
})
}
function findFlows(options: { [key: string]: any }) {
const sortString = options.sort ? `?sort=${options.sort}` : ""
delete options.sort
return axios.get(`${apiUrl()}/flows/search${sortString}`, {
params: options
}).then(response => {
function findFlows(options: Parameters<typeof sdk.flowsSearchFlows>[0] & { onlyTotal?: boolean }) {
return sdk.flowsSearchFlows(options).then(response => {
if(!response.data){
return undefined
}
if (options.onlyTotal) {
return response.data.total;
}
else {
flows.value = response.data.results
flows.value = response.data?.results
total.value = response.data.total
overallTotal.value = response.data.results.filter((f: any) => f.namespace !== "tutorial").length
@@ -340,20 +329,22 @@ export const useFlowStore = defineStore("flow", () => {
})
}
function loadFlow(options: { namespace: string, id: string, revision?: string, allowDeleted?: boolean, source?: boolean, store?: boolean, deleted?: boolean, httpClient?: any }) {
const httpClient = options.httpClient ?? axios
return httpClient.get(`${apiUrl()}/flows/${options.namespace}/${options.id}`,
{
params: {
function loadFlow(options: { namespace: string, id: string, revision?: number, allowDeleted?: boolean, source?: boolean, store?: boolean, deleted?: boolean, httpClient?: any }) {
const httpClient = options.httpClient
return sdk.flowsGetFlow({
id: options.id,
namespace: options.namespace,
revision: options.revision,
allowDeleted: options.allowDeleted,
source: options.source === undefined ? true : undefined
},
validateStatus: (status: number) => {
return options.deleted ? status === 200 || status === 404 : status === 200;
allowDeleted: options.allowDeleted ?? false,
source: options.source === undefined ? true : false
}
, {
client: httpClient
})
.then((response: any) => {
.then((response) => {
if(!response.data){
return Promise.reject("Flow not found");
}
if (response.data.exception) {
coreStore.message = {
title: "Invalid source code",
@@ -373,8 +364,8 @@ export const useFlowStore = defineStore("flow", () => {
}
flow.value = response.data;
flowYaml.value = response.data.source;
flowYamlOrigin.value = response.data.source;
flowYaml.value = response.data.source ?? "";
flowYamlOrigin.value = response.data.source ?? "";
overallTotal.value = 1;
return response.data;
@@ -430,11 +421,11 @@ export const useFlowStore = defineStore("flow", () => {
}
function createFlow(options: { flow: string }) {
return axios.post(`${apiUrl()}/flows`, options.flow, {
return sdk.flowsCreateFlow({body: options.flow}, {
...textYamlHeader,
...VALIDATE
}).then(response => {
if (response.status >= 300) {
if (!response?.status || response.status >= 300) {
return Promise.reject(response)
}
@@ -653,7 +644,12 @@ function deleteFlowAndDependencies() {
return axios.delete(`${apiUrl()}/flows/delete/by-query`, {params: options})
}
function validateFlow(options: { flow: string }) {
function validateFlow(options: { flow?: string }) {
if(!options.flow) {
return Promise.resolve({
constraints: t("flow must not be empty")
});
}
const flowValidationIssues: FlowValidations = {};
if(isCreating.value) {
const {namespace} = YAML_UTILS.getMetadata(options.flow);

View File

@@ -1,10 +1,6 @@
@import "@kestra-io/ui-libs/src/scss/variables.scss";
#app {
.v-sidebar-menu.vsm_expanded.overlay {
position: absolute;
}
.vsm--item {
padding: 0 30px;
transition: padding 0.2s ease;

View File

@@ -883,12 +883,6 @@
"description": "Nach Trigger-ID filtern",
"label": "Trigger-ID"
},
"triggerState": {
"description": "Nach Trigger-Zustand filtern",
"disabled": "Deaktiviert",
"enabled": "Aktiviert",
"label": "Trigger-Zustand"
},
"update": "Aktualisieren",
"value": "Wert",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Weiter",
"no_flows": "Keine Flows unter der tutorial Namespace verfügbar.",
"previous": "Zurück",
"skip": "Produkt-Tour überspringen",
"skip": "Tutorial überspringen",
"steps": {
"0": {
"content": "Wir freuen uns, dass Sie hier sind.<br />Lassen Sie uns Ihren ersten Flow erstellen.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Willkommen bei Kestra!",
"welcome aboard content": "Nutzen Sie unsere geführte Tour, um Ihren ersten Flow zu erstellen, und schauen Sie sich Blueprints an, um weitere Beispiele zu finden.",
"welcome button create": "Produkt-Tour starten",
"welcome button create": "Meinen ersten Flow erstellen",
"welcome display require": "Führen Sie Ihren <strong>ersten Flow</strong> aus, um loszulegen",
"welcome_page": {
"guide": "Benötigen Sie Unterstützung, um Ihren ersten flow auszuführen?",

View File

@@ -540,7 +540,7 @@
"welcome aboard": "\uD83D\uDE80 Welcome to Kestra!",
"welcome aboard content": "Use our Guided Tour to create your first flow and check Blueprints to find more examples.",
"welcome display require": "Run your <strong>first flow</strong> to get started",
"welcome button create": "Start Product Tour",
"welcome button create": "Create my first flow",
"live help": "Live help",
"show task documentation": "Show task documentation",
"hide task documentation": "Hide task documentation",
@@ -947,7 +947,7 @@
"next": "Next",
"previous": "Previous",
"finish": "Finish",
"skip": "Skip Product Tour",
"skip": "Skip Tutorial",
"no_flows": "No flows available under tutorial namespace.",
"steps": {
"0": {

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID de Trigger"
},
"triggerState": {
"description": "Filtrar por estado del trigger",
"disabled": "Desactivado",
"enabled": "Habilitado",
"label": "Estado del Trigger"
},
"update": "Actualizar",
"value": "Valor",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Siguiente",
"no_flows": "No hay flows disponibles bajo el namespace del tutorial.",
"previous": "Anterior",
"skip": "Omitir recorrido del producto",
"skip": "Saltar Tutorial",
"steps": {
"0": {
"content": "Estamos encantados de tenerte aquí.<br />Vamos a crear tu primer flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 ¡Bienvenido a Kestra!",
"welcome aboard content": "Usa nuestro Tour Guiado para crear tu primer flow y revisa los Blueprints para encontrar más ejemplos.",
"welcome button create": "Iniciar Tour del Producto",
"welcome button create": "Crear mi primer flow",
"welcome display require": "Ejecuta tu <strong>primer flow</strong> para comenzar",
"welcome_page": {
"guide": "¿Necesitas orientación para ejecutar tu primer flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrer par trigger ID",
"label": "ID du trigger"
},
"triggerState": {
"description": "Filtrer par état du trigger",
"disabled": "Désactivé",
"enabled": "Activé",
"label": "État du Trigger"
},
"update": "Mettre à jour",
"value": "Valeur",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Suivant",
"no_flows": "Aucun flux disponible dans l'espace de noms du tutoriel.",
"previous": "Précédent",
"skip": "Passer la visite guidée du produit",
"skip": "Passer le tutoriel",
"steps": {
"0": {
"content": "Nous sommes ravis de vous avoir ici.<br />Créons votre premier flux.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Bienvenue à bord !",
"welcome aboard content": "Tout est prêt pour Kestra, commencez la création de votre flow et admirez la magie !",
"welcome button create": "Démarrer la visite guidée du produit",
"welcome button create": "Créer mon premier flow",
"welcome display require": "Prêt à commencer à utiliser Kestra ? Créons ensemble <strong>votre premier flow</strong> !",
"welcome_page": {
"guide": "Besoin d'aide pour exécuter votre premier flow ?",

View File

@@ -883,12 +883,6 @@
"description": "ट्रिगर ID द्वारा फ़िल्टर करें",
"label": "ट्रिगर ID"
},
"triggerState": {
"description": "ट्रिगर स्थिति द्वारा फ़िल्टर करें",
"disabled": "अक्षम",
"enabled": "सक्रिय",
"label": "ट्रिगर स्थिति"
},
"update": "अपडेट",
"value": "मान",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "अगला",
"no_flows": "ट्यूटोरियल namespace के अंतर्गत कोई flows उपलब्ध नहीं हैं।",
"previous": "पिछला",
"skip": "उत्पाद टूर छोड़ें",
"skip": "ट्यूटोरियल छोड़ें",
"steps": {
"0": {
"content": "हमें खुशी है कि आप यहाँ हैं।<br />आइए अपना पहला flow बनाएं।",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 केस्ट्रा में आपका स्वागत है!",
"welcome aboard content": "अपना पहला flow बनाने के लिए हमारे Guided Tour का उपयोग करें और अधिक उदाहरण खोजने के लिए Blueprints देखें।",
"welcome button create": "उत्पाद यात्रा शुरू करें",
"welcome button create": "मेरा पहला flow बनाएं",
"welcome display require": "शुरू करने के लिए अपना <strong>पहला flow</strong> चलाएँ",
"welcome_page": {
"guide": "क्या आपको अपना पहला flow निष्पादित करने के लिए मार्गदर्शन चाहिए?",

View File

@@ -883,12 +883,6 @@
"description": "Filtra per trigger ID",
"label": "ID del trigger"
},
"triggerState": {
"description": "Filtra per stato del trigger",
"disabled": "Disabilitato",
"enabled": "Abilitato",
"label": "Stato del Trigger"
},
"update": "Aggiorna",
"value": "Valore",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Successivo",
"no_flows": "Nessun flow disponibile sotto il namespace del tutorial.",
"previous": "Precedente",
"skip": "Salta il Tour del Prodotto",
"skip": "Salta Tutorial",
"steps": {
"0": {
"content": "Siamo entusiasti di averti qui.<br />Creiamo il tuo primo flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Benvenuto su Kestra!",
"welcome aboard content": "Usa il nostro Tour Guidato per creare il tuo primo flow e controlla i Blueprints per trovare altri esempi.",
"welcome button create": "Inizia il Tour del Prodotto",
"welcome button create": "Crea il mio primo flow",
"welcome display require": "Esegui il tuo <strong>primo flow</strong> per iniziare",
"welcome_page": {
"guide": "Hai bisogno di assistenza per eseguire il tuo primo flow?",

View File

@@ -883,12 +883,6 @@
"description": "トリガーIDでフィルター",
"label": "トリガーID"
},
"triggerState": {
"description": "トリガー状態でフィルター",
"disabled": "無効",
"enabled": "有効",
"label": "トリガー状態"
},
"update": "更新",
"value": "値",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "次へ",
"no_flows": "チュートリアルnamespaceに利用可能なflowはありません。",
"previous": "前へ",
"skip": "プロダクトツアーをスキップ",
"skip": "チュートリアルをスキップ",
"steps": {
"0": {
"content": "ここに来てくれてとても嬉しいです。<br />最初のflowを作成しましょう。",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Kestraへようこそ",
"welcome aboard content": "ガイド付きツアーを利用して最初のflowを作成し、Blueprintsでさらに多くの例を見つけてください。",
"welcome button create": "プロダクトツアーを開始",
"welcome button create": "最初のflowを作成",
"welcome display require": "<strong>最初のflow</strong>を実行して始めましょう",
"welcome_page": {
"guide": "最初のflowを実行するためのガイダンスが必要ですか",

View File

@@ -883,12 +883,6 @@
"description": "트리거 ID로 필터링",
"label": "트리거 ID"
},
"triggerState": {
"description": "트리거 상태별 필터링",
"disabled": "비활성화됨",
"enabled": "사용 가능",
"label": "트리거 상태"
},
"update": "업데이트",
"value": "값",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "다음",
"no_flows": "튜토리얼 namespace에 사용할 수 있는 flow가 없습니다.",
"previous": "이전",
"skip": "제품 투어 건너뛰기",
"skip": "튜토리얼 건너뛰기",
"steps": {
"0": {
"content": "여기 오신 것을 환영합니다.<br />첫 번째 flow를 만들어 봅시다.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Kestra에 오신 것을 환영합니다!",
"welcome aboard content": "가이드 투어를 사용하여 첫 번째 flow를 만들고 Blueprints에서 더 많은 예제를 확인하세요.",
"welcome button create": "제품 투어 시작",
"welcome button create": "첫 번째 flow 만들기",
"welcome display require": "<strong>첫 번째 flow</strong>를 실행하여 시작하세요",
"welcome_page": {
"guide": "첫 번째 flow를 실행하는 데 도움이 필요하신가요?",

View File

@@ -883,12 +883,6 @@
"description": "Filtruj według trigger ID",
"label": "Identyfikator Trigger"
},
"triggerState": {
"description": "Filtruj według stanu triggera",
"disabled": "Wyłączone",
"enabled": "Włączone",
"label": "Stan Trigger"
},
"update": "Aktualizuj",
"value": "Wartość",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Następny",
"no_flows": "Brak flowów w namespace o nazwie tutorial.",
"previous": "Poprzedni",
"skip": "Pomiń Przewodnik po Produkcie",
"skip": "Pomiń samouczek",
"steps": {
"0": {
"content": "Jesteśmy zachwyceni, że jesteś tutaj.<br />Stwórzmy twój pierwszy flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Witamy w Kestra!",
"welcome aboard content": "Skorzystaj z naszego Przewodnika, aby stworzyć swój pierwszy flow i sprawdź Blueprints, aby znaleźć więcej przykładów.",
"welcome button create": "Rozpocznij Przewodnik po Produkcie",
"welcome button create": "Stwórz mój pierwszy flow",
"welcome display require": "Uruchom swój <strong>pierwszy flow</strong>, aby rozpocząć",
"welcome_page": {
"guide": "Potrzebujesz wskazówek, jak uruchomić swój pierwszy flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID do Trigger"
},
"triggerState": {
"description": "Filtrar por estado do trigger",
"disabled": "Desativado",
"enabled": "Habilitado",
"label": "Estado do Trigger"
},
"update": "Atualizar",
"value": "Valor",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Próximo",
"no_flows": "Não há flows disponíveis no namespace do tutorial.",
"previous": "Anterior",
"skip": "Pular Tour do Produto",
"skip": "Pular Tutorial",
"steps": {
"0": {
"content": "Estamos entusiasmados em tê-lo aqui.<br />Vamos criar seu primeiro flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Bem-vindo ao Kestra!",
"welcome aboard content": "Use nosso Tour Guiado para criar seu primeiro flow e confira os Blueprints para encontrar mais exemplos.",
"welcome button create": "Iniciar Tour do Produto",
"welcome button create": "Criar meu primeiro flow",
"welcome display require": "Execute seu <strong>primeiro flow</strong> para começar",
"welcome_page": {
"guide": "Precisa de orientação para executar seu primeiro flow?",

View File

@@ -883,12 +883,6 @@
"description": "Filtrar por trigger ID",
"label": "ID do Trigger"
},
"triggerState": {
"description": "Filtrar por estado do trigger",
"disabled": "Desativado",
"enabled": "Habilitado",
"label": "Estado do Trigger"
},
"update": "Atualizar",
"value": "Valor",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Próximo",
"no_flows": "Não há flows disponíveis no namespace do tutorial.",
"previous": "Anterior",
"skip": "Pular Tour do Produto",
"skip": "Pular Tutorial",
"steps": {
"0": {
"content": "Estamos entusiasmados em tê-lo aqui.<br />Vamos criar seu primeiro flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Bem-vindo ao Kestra!",
"welcome aboard content": "Use nosso Tour Guiado para criar seu primeiro flow e confira os Blueprints para encontrar mais exemplos.",
"welcome button create": "Iniciar Tour do Produto",
"welcome button create": "Criar meu primeiro flow",
"welcome display require": "Execute seu <strong>primeiro flow</strong> para começar",
"welcome_page": {
"guide": "Precisa de orientação para executar seu primeiro flow?",

View File

@@ -883,12 +883,6 @@
"description": "Фильтр по trigger ID",
"label": "ID триггера"
},
"triggerState": {
"description": "Фильтр по состоянию trigger",
"disabled": "Отключено",
"enabled": "Включено",
"label": "Состояние Trigger"
},
"update": "Обновить",
"value": "Значение",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "Далее",
"no_flows": "Нет доступных flows в namespace учебника.",
"previous": "Назад",
"skip": "Пропустить ознакомительный тур с продуктом",
"skip": "Пропустить учебник",
"steps": {
"0": {
"content": "Мы рады видеть вас здесь.<br />Давайте создадим ваш первый flow.",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 Добро пожаловать в Kestra!",
"welcome aboard content": "Используйте наше Руководство, чтобы создать ваш первый flow и ознакомьтесь с Blueprints для поиска дополнительных примеров.",
"welcome button create": "Начать ознакомительный тур с продуктом",
"welcome button create": "Создать мой первый flow",
"welcome display require": "Запустите ваш <strong>первый flow</strong>, чтобы начать",
"welcome_page": {
"guide": "Нужна помощь в выполнении вашего первого flow?",

View File

@@ -883,12 +883,6 @@
"description": "按 trigger ID 筛选",
"label": "触发器 ID"
},
"triggerState": {
"description": "按触发器状态筛选",
"disabled": "禁用",
"enabled": "启用",
"label": "触发状态"
},
"update": "更新",
"value": "值",
"workerId": {
@@ -1314,7 +1308,7 @@
"next": "下一步",
"no_flows": "教程命名空间下没有可用的流程。",
"previous": "上一步",
"skip": "跳过产品指南",
"skip": "跳过教程",
"steps": {
"0": {
"content": "我们很高兴你在这里。<br />让我们创建你的第一个流程。",
@@ -1863,7 +1857,7 @@
},
"welcome aboard": "🚀 欢迎使用 Kestra!",
"welcome aboard content": "使用我们的引导游览来创建你的第一个流程,并查看蓝图以找到更多示例。",
"welcome button create": "开始产品导览",
"welcome button create": "创建我的第一个流程",
"welcome display require": "运行你的 <strong>第一个流程</strong> 以开始",
"welcome_page": {
"guide": "需要指导来执行您的第一个flow吗",

View File

@@ -1,4 +1,4 @@
import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, AxiosProgressEvent} from "axios"
import axios, {AxiosRequestConfig, AxiosResponse, AxiosError, AxiosProgressEvent} from "axios"
import NProgress from "nprogress"
import {Router, useRouter} from "vue-router"
import {storageKeys} from "./constants"
@@ -8,6 +8,9 @@ import * as BasicAuth from "../utils/basicAuth"
import {useAuthStore} from "override/stores/auth"
import {useMiscStore} from "override/stores/misc";
import {useUnsavedChangesStore} from "../stores/unsavedChanges"
import {client} from "../generated/kestra-api/client.gen"
import {Client} from "../generated/kestra-api/client"
export * as sdk from "../generated/kestra-api/sdk.gen"
let pendingRoute = false
let requestsTotal = 0
@@ -72,7 +75,7 @@ interface QueueItem {
resolve: (value: AxiosResponse | Promise<AxiosResponse>) => void
}
export const createAxios = (
const createAxios = (
router: Router | undefined,
oss: boolean
) => {
@@ -283,28 +286,38 @@ export const createAxios = (
}
})
return instance;
client.setConfig({
axios: instance
})
return client;
};
export default (
callback: (instance: AxiosInstance) => void,
callback: (clientInstance: Client["instance"]) => void,
_store: any,
...args: Parameters<typeof createAxios>
) => {
callback(createAxios(...args));
callback(createAxios(...args).instance);
}
let axiosInstance: AxiosInstance | null = null;
let clientInstance: Client | null = null;
export const useAxios = () => {
export function useClient(){
const router = useRouter();
const miscStore = useMiscStore();
const {edition} = miscStore.configs || {};
if (!axiosInstance) {
axiosInstance = createAxios(router, edition === "OSS");
if (!clientInstance) {
clientInstance = createAxios(router, edition === "OSS");
}
return axiosInstance;
return clientInstance;
};
export function useAxios(){
const clientInstance = useClient();
return clientInstance.instance;
};