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
15 changed files with 660 additions and 92 deletions

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",
})
],
});

299
ui/package-lock.json generated
View File

@@ -67,6 +67,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",
@@ -1982,6 +1983,118 @@
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@hey-api/codegen-core": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.4.0.tgz",
"integrity": "sha512-o8rBbEXEUhEPzrHbqImYjwIHm4Oj0r1RPS+5cp8Z66kPO7SEN7PYUgK7XpmSxoy9LPMNK1M5qmCO4cGGwT+ELQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-colors": "4.1.3",
"color-support": "1.1.3"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
},
"peerDependencies": {
"typescript": ">=5.5.3"
}
},
"node_modules/@hey-api/json-schema-ref-parser": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.2.tgz",
"integrity": "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
}
},
"node_modules/@hey-api/openapi-ts": {
"version": "0.89.2",
"resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.89.2.tgz",
"integrity": "sha512-iEhWfvPbzfcS7BMqHzh2FbMG1ouZVAHwHoYhq/61Rmq/r1Q1NgCZ3xy6QuXZgbTdHe9enMjWXjINLnTj57kWwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@hey-api/codegen-core": "^0.4.0",
"@hey-api/json-schema-ref-parser": "1.2.2",
"ansi-colors": "4.1.3",
"c12": "3.3.2",
"color-support": "1.1.3",
"commander": "14.0.2",
"open": "11.0.0",
"semver": "7.7.3"
},
"bin": {
"openapi-ts": "bin/run.js"
},
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/sponsors/hey-api"
},
"peerDependencies": {
"typescript": ">=5.5.3"
}
},
"node_modules/@hey-api/openapi-ts/node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/@hey-api/openapi-ts/node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@hey-api/openapi-ts/node_modules/open": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"default-browser": "^5.4.0",
"define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
"powershell-utils": "^0.1.0",
"wsl-utils": "^0.3.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2831,6 +2944,13 @@
"integrity": "sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==",
"license": "BSD-3-Clause"
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.268",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.268.tgz",
@@ -7149,6 +7269,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -7793,6 +7923,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"run-applescript": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/c12": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.2.tgz",
@@ -8329,6 +8475,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"dev": true,
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@@ -9278,6 +9434,36 @@
"node": ">=0.10.0"
}
},
"node_modules/default-browser": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz",
"integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
"dev": true,
"license": "MIT",
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-require-extensions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
@@ -12070,6 +12256,54 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-in-ssh": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container/node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -17102,6 +17336,19 @@
"web-vitals": "^4.2.4"
}
},
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/preact": {
"version": "10.27.2",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
@@ -18713,6 +18960,19 @@
"points-on-path": "^0.2.1"
}
},
"node_modules/run-applescript": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -19216,9 +19476,9 @@
"license": "MIT"
},
"node_modules/storybook": {
"version": "9.1.16",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.16.tgz",
"integrity": "sha512-339U14K6l46EFyRvaPS2ZlL7v7Pb+LlcXT8KAETrGPxq8v1sAjj2HAOB6zrlAK3M+0+ricssfAwsLCwt7Eg8TQ==",
"version": "9.1.17",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.17.tgz",
"integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -21753,6 +22013,39 @@
}
}
},
"node_modules/wsl-utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.0.tgz",
"integrity": "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^3.1.0",
"powershell-utils": "^0.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wsl-utils/node_modules/is-wsl": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",

View File

@@ -20,7 +20,8 @@
"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",
@@ -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",

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

@@ -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

@@ -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,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;
};