Compare commits

...

12 Commits

Author SHA1 Message Date
nKwiatkowski
c1f81dec80 fix(queue): change index to remove a sort 2025-12-08 17:06:21 +01:00
Loïc Mathieu
424a6cb41a fix(execution): skip the render cache in flowable for properties used to compute next tasks
As when the flowable is itself in a flowable that process tasks concurrently like the ForEach when using a concurrency limit, it can be done multiple time with different values.

This can only occurs if the expression is using `taskRun.value`.

Fixes https://github.com/kestra-io/kestra-ee/issues/6055
2025-12-08 15:03:06 +01:00
Debjyoti Shit
afde71e913 fix(core): skip login screen after initial setup and send to welcome (#13489)
Co-authored-by: Miloš Paunović <paun992@hotmail.com>
Co-authored-by: Piyush Bhaskar <impiyush0012@gmail.com>
2025-12-08 18:09:50 +05:30
Miloš Paunović
086c32e711 chore(flows): redirect with applied filters from the overview page (#13522)
Closes https://github.com/kestra-io/kestra/issues/13392.
2025-12-08 10:52:46 +01:00
github-actions[bot]
710abcfaac chore(core): localize to languages other than english (#13520)
Co-authored-by: GitHub Action <actions@github.com>
2025-12-08 14:26:03 +05:30
Kavyakapoor
be951d015c fix(core): make password requirement descriptive. (#13483) 2025-12-08 14:15:11 +05:30
Piyush Bhaskar
a07260bef4 fix(core): refine navigation for authentication and setup routes (#13517) 2025-12-08 14:13:28 +05:30
Piyush Bhaskar
dd19f8391d chore(version): bump ui-libs (#13518) 2025-12-08 14:11:59 +05:30
mustafatarek
354873e220 chore(core): remove unnecessary attempt list copying 2025-12-08 09:41:10 +01:00
luoxin
386d4a15f0 fix(system): enable parallel loading for namespace files. (#13375)
* perf(core): enable parallel loading for namespace files.

* refactor(core): extract thread count calculation to avoid duplication.

* resolve namespaceFilesWithNamespaces test error.

---------

Co-authored-by: luoxin5 <luoxin5@xiaomi.com>
2025-12-08 09:23:35 +01:00
Yaswanth B
1b75f15680 refactor(core): remove usage of unnecessary i18n composable (#13492)
Closes https://github.com/kestra-io/kestra/issues/13352.

Co-authored-by: MilosPaunovic <paun992@hotmail.com>
2025-12-08 08:42:56 +01:00
Richard-Mackey
957bf74d97 fix(core): make menuCollapsed = true on small screen (#13238)
Co-authored-by: Piyush Bhaskar <impiyush0012@gmail.com>
2025-12-06 02:58:37 +05:30
34 changed files with 146 additions and 62 deletions

View File

@@ -93,7 +93,7 @@ public class Property<T> {
* @return a new {@link Property} without a pre-rendered value
*/
public Property<T> skipCache() {
return Property.ofExpression(expression);
return new Property<>(expression, true);
}
/**

View File

@@ -32,10 +32,12 @@ public class NamespaceFilesUtils {
private ExecutorsUtils executorsUtils;
private ExecutorService executorService;
private int maxThreads;
@PostConstruct
public void postConstruct() {
this.executorService = executorsUtils.maxCachedThreadPool(Math.max(Runtime.getRuntime().availableProcessors() * 4, 32), "namespace-file");
this.maxThreads = Math.max(Runtime.getRuntime().availableProcessors() * 4, 32);
this.executorService = executorsUtils.maxCachedThreadPool(maxThreads, "namespace-file");
}
public void loadNamespaceFiles(
@@ -63,7 +65,11 @@ public class NamespaceFilesUtils {
matchedNamespaceFiles.addAll(files);
}
// Use half of the available threads to avoid impacting concurrent tasks
int parallelism = maxThreads / 2;
Flux.fromIterable(matchedNamespaceFiles)
.parallel(parallelism)
.runOn(Schedulers.fromExecutorService(executorService))
.doOnNext(throwConsumer(nsFile -> {
InputStream content = runContext.storage().getFile(nsFile.uri());
Path path = folderPerNamespace ?
@@ -71,7 +77,7 @@ public class NamespaceFilesUtils {
Path.of(nsFile.path());
runContext.workingDir().putFile(path, content, fileExistComportment);
}))
.publishOn(Schedulers.fromExecutorService(executorService))
.sequential()
.blockLast();
Duration duration = stopWatch.getDuration();

View File

@@ -157,7 +157,7 @@ public class LoopUntil extends Task implements FlowableTask<LoopUntil.Output> {
public Instant nextExecutionDate(RunContext runContext, Execution execution, TaskRun parentTaskRun) throws IllegalVariableEvaluationException {
if (!this.reachedMaximums(runContext, execution, parentTaskRun, false)) {
String continueLoop = runContext.render(this.condition).as(String.class).orElse(null);
String continueLoop = runContext.render(this.condition).skipCache().as(String.class).orElse(null);
if (!TruthUtils.isTruthy(continueLoop)) {
return Instant.now().plus(runContext.render(this.getCheckFrequency().getInterval()).as(Duration.class).orElseThrow());
}

View File

@@ -123,7 +123,7 @@ public class Switch extends Task implements FlowableTask<Switch.Output> {
}
private String rendererValue(RunContext runContext) throws IllegalVariableEvaluationException {
return runContext.render(this.value).as(String.class).orElseThrow();
return runContext.render(this.value).skipCache().as(String.class).orElseThrow();
}
@Override

View File

@@ -1,9 +1,11 @@
package io.kestra.plugin.core.flow;
import static io.kestra.core.tenant.TenantService.MAIN_TENANT;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.junit.annotations.ExecuteFlow;
import io.kestra.core.junit.annotations.KestraTest;
import io.kestra.core.junit.annotations.LoadFlows;
import io.kestra.core.models.executions.Execution;
@@ -100,4 +102,14 @@ class SwitchTest {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.FAILED);
}
@Test
@ExecuteFlow("flows/valids/switch-in-concurrent-loop.yaml")
void switchInConcurrentLoop(Execution execution) {
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
assertThat(execution.getTaskRunList()).hasSize(5);
// we check that OOMCRM_EB_DD_000 and OOMCRM_EB_DD_001 have been processed once
assertThat(execution.getTaskRunList().stream().filter(t -> t.getTaskId().equals("OOMCRM_EB_DD_000")).count()).isEqualTo(1);
assertThat(execution.getTaskRunList().stream().filter(t -> t.getTaskId().equals("OOMCRM_EB_DD_001")).count()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,23 @@
id: switch-in-concurrent-loop
namespace: io.kestra.tests
tasks:
- id: iterate_and_check_name
type: io.kestra.plugin.core.flow.ForEach
tasks:
- id: switch
type: io.kestra.plugin.core.flow.Switch
value: "{{ taskrun.value }}"
cases:
"Alice":
- id: OOMCRM_EB_DD_000
type: io.kestra.plugin.core.log.Log
message: Alice
"Bob":
- id: OOMCRM_EB_DD_001
type: io.kestra.plugin.core.log.Log
message: Bob
values: ["Alice", "Bob"]
concurrencyLimit: 0

View File

@@ -13,18 +13,19 @@ tasks:
- io.test.second
- io.test.third
enabled: true
folderPerNamespace: true
exclude:
- /ignore/**
tasks:
- id: t1
type: io.kestra.core.tasks.test.Read
path: "/test/a/b/c/1.txt"
path: "/io.test.third/test/a/b/c/1.txt"
- id: t2
type: io.kestra.core.tasks.test.Read
path: "/a/b/c/2.txt"
path: "/io.test.second/a/b/c/2.txt"
- id: t3
type: io.kestra.core.tasks.test.Read
path: "/a/b/3.txt"
path: "/io.test.first/a/b/3.txt"
- id: t4
type: io.kestra.core.tasks.test.Read
path: "/ignore/4.txt"

View File

@@ -36,7 +36,7 @@ public class MysqlQueue<T> extends JdbcQueue<T> {
AbstractJdbcRepository.field("offset")
)
// force using the dedicated index, or it made a scan of the PK index
.from(this.table.useIndex("ix_type__consumers"))
.from(this.table.useIndex("ix_type__offset"))
.where(AbstractJdbcRepository.field("type").eq(queueType()))
.and(DSL.or(List.of(
AbstractJdbcRepository.field("consumers").isNull(),

View File

@@ -176,7 +176,7 @@ public class JdbcExecutor implements ExecutorInterface {
@Inject
private AbstractJdbcWorkerJobRunningRepository workerJobRunningRepository;
@Inject
private SLAMonitorStorage slaMonitorStorage;
@@ -658,21 +658,16 @@ public class JdbcExecutor implements ExecutorInterface {
workerTaskResults.add(new WorkerTaskResult(taskRun));
}
}
/// flowable attempt state transition to running
// flowable attempt state transition to running
if (workerTask.getTask().isFlowable()) {
List<TaskRunAttempt> attempts = Optional.ofNullable(workerTask.getTaskRun().getAttempts())
.map(ArrayList::new)
.orElseGet(ArrayList::new);
attempts.add(
TaskRunAttempt.builder()
.state(new State().withState(State.Type.RUNNING))
.build()
);
TaskRun updatedTaskRun = workerTask.getTaskRun()
.withAttempts(attempts)
.withAttempts(
List.of(
TaskRunAttempt.builder()
.state(new State().withState(State.Type.RUNNING))
.build()
)
)
.withState(State.Type.RUNNING);
workerTaskResults.add(new WorkerTaskResult(updatedTaskRun));

8
ui/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.263",
"@kestra-io/ui-libs": "^0.0.264",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.47.0",
@@ -2829,9 +2829,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@kestra-io/ui-libs": {
"version": "0.0.263",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.263.tgz",
"integrity": "sha512-j1rWqcQAK2CudNBkcDPjUXyaGFeBzJ7QEhPKFAbleHSw0N3QFu/iy0rFZxJNIMWRi1mGZBh74D6vL0OqQJkT2Q==",
"version": "0.0.264",
"resolved": "https://registry.npmjs.org/@kestra-io/ui-libs/-/ui-libs-0.0.264.tgz",
"integrity": "sha512-yUZDNaE0wUPOuEq/FL/TQBRd1fTV2dyM8s+VcGRjNSM1uv1uZcsSHro56/heHQx17lo00FDcPT7BMKEifrVhBg==",
"dependencies": {
"@nuxtjs/mdc": "^0.17.3",
"@popperjs/core": "^2.11.8",

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@js-joda/core": "^5.6.5",
"@kestra-io/ui-libs": "^0.0.263",
"@kestra-io/ui-libs": "^0.0.264",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.47.0",

View File

@@ -86,8 +86,8 @@
</el-input>
</el-form-item>
<div class="password-requirements mb-4">
<el-text>
8+ chars, 1 upper, 1 number
<el-text>
{{ t('setup.form.password_requirements') }}
</el-text>
</div>
</el-form>
@@ -502,7 +502,7 @@
localStorage.removeItem("basicAuthUserCreated")
localStorage.setItem("basicAuthSetupCompletedAt", new Date().toISOString())
router.push({name: "login"})
router.push({name: "welcome"})
}
</script>

View File

@@ -101,7 +101,7 @@ $checkbox-checked-color: #8405FF;
.el-text {
color: var(--ks-content-tertiary);
font-size: 14px;
font-size: 12px;
}
}

View File

@@ -3,6 +3,8 @@ import Utils from "../../../utils/utils";
import {cssVariable, State} from "@kestra-io/ui-libs";
import {getSchemeValue} from "../../../utils/scheme";
import {useMiscStore} from "override/stores/misc";
export function tooltip(tooltipModel: {
title?: string[];
body?: { lines: string[] }[];
@@ -115,7 +117,7 @@ export function extractState(value: any) {
return value;
}
export function chartClick(moment: any, router: any, route: any, event: any, parsedData: any, elements: any, type = "label") {
export function chartClick(moment: any, router: any, route: any, event: any, parsedData: any, elements: any, type = "label", filters: Record<string, any> = {}) {
const query: Record<string, any> = {};
if (elements && parsedData) {
@@ -192,7 +194,11 @@ export function chartClick(moment: any, router: any, route: any, event: any, par
params: {
tenant: route.params.tenant,
},
query: query,
query: {
...query,
...filters,
"filters[timeRange][EQUALS]":useMiscStore()?.configs?.chartDefaultDuration ?? "P30D"
},
});
}
}

View File

@@ -49,6 +49,8 @@
showDefault: {type: Boolean, default: false},
short: {type: Boolean, default: false},
execution: {type: Boolean, default: false},
flow: {type: String, default: undefined},
namespace: {type: String, default: undefined},
});
@@ -153,7 +155,10 @@
if (data.type === "io.kestra.plugin.core.dashboard.data.Logs" || props.execution) {
return;
}
chartClick(moment, router, route, {}, parsedData.value, elements, "label");
chartClick(moment, router, route, {}, parsedData.value, elements, "label", {
...(props.namespace ? {"filters[namespace][IN]": props.namespace} : {}),
...(props.flow ? {"filters[flowId][EQUALS]": props.flow} : {})
});
},
}, theme.value);
});

View File

@@ -213,6 +213,8 @@
:filters="chartFilters()"
showDefault
short
:flow="scope.row.id"
:namespace="scope.row.namespace"
/>
</template>
</el-table-column>

View File

@@ -7,14 +7,14 @@
:disabled="!playgroundStore.readyToStart"
>
<el-icon><Play /></el-icon>
<span>{{ t('playground.run_task') }}</span>
<span>{{ $t('playground.run_task') }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="Play" @click="playgroundStore.runUntilTask(taskId)">
{{ t('playground.run_this_task') }}
{{ $t('playground.run_this_task') }}
</el-dropdown-item>
<el-dropdown-item :icon="PlayBoxMultiple" @click="playgroundStore.runUntilTask(taskId, true)">
{{ t('playground.run_task_and_downstream') }}
{{ $t('playground.run_task_and_downstream') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -22,12 +22,10 @@
</template>
<script setup lang="ts">
import {useI18n} from "vue-i18n";
import {usePlaygroundStore} from "../../stores/playground";
import Play from "vue-material-design-icons/Play.vue";
import PlayBoxMultiple from "vue-material-design-icons/PlayBoxMultiple.vue";
const {t} = useI18n();
const playgroundStore = usePlaygroundStore();
defineProps<{

View File

@@ -12,6 +12,7 @@
import {useCoreStore} from "../../stores/core";
import {useMiscStore} from "override/stores/misc";
import {computed, onMounted} from "vue";
import {useLayoutStore} from "../../stores/layout";
const coreStore = useCoreStore();
const miscStore = useMiscStore();
@@ -22,7 +23,9 @@
document.getElementsByTagName("html")[0].classList.remove(collapse ? "menu-not-collapsed" : "menu-collapsed");
}
const layoutStore = useLayoutStore();
onMounted(() => {
onMenuCollapse(localStorage.getItem("menuCollapsed") === "true")
onMenuCollapse(Boolean(layoutStore.sideMenuCollapsed))
});
</script>

View File

@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import {onUpdated, ref, computed, h, watch} from "vue";
import {onUpdated, computed, h, watch} from "vue";
import {useI18n} from "vue-i18n";
import {useRoute} from "vue-router";
import {useMediaQuery} from "@vueuse/core";
@@ -118,7 +118,10 @@
];
});
const collapsed = ref(localStorage.getItem("menuCollapsed") === "true")
const collapsed = computed({
get: () => layoutStore.sideMenuCollapsed,
set: (v: boolean) => layoutStore.setSideMenuCollapsed(v),
})
const isSmallScreen = useMediaQuery("(max-width: 768px)")

View File

@@ -25,10 +25,6 @@ const handleAuthError = (error, to) => {
initApp(app, routes, null, en).then(({router, piniaStore}) => {
router.beforeEach(async (to, from, next) => {
if (to.meta?.anonymous === true) {
return next();
}
if(to.path === from.path && to.query === from.query) {
return next(); // Prevent navigation if the path and query are the same
}
@@ -45,13 +41,28 @@ initApp(app, routes, null, en).then(({router, piniaStore}) => {
if (validationErrors?.length > 0) {
// Creds exist in config but failed validation
// Route to login to show errors
if (to.name === "login") {
return next();
}
return next({name: "login"})
} else {
// No creds in config - redirect to set it up
if (to.name === "setup") {
return next();
}
return next({name: "setup"})
}
}
if (to.meta?.anonymous === true) {
if (to.name === "setup") {
return next({name: "login"});
}
return next();
}
const hasCredentials = BasicAuth.isLoggedIn()
if (!hasCredentials) {
@@ -92,6 +103,6 @@ initApp(app, routes, null, en).then(({router, piniaStore}) => {
}, null, router, true);
// mount
app.mount("#app")
router.isReady().then(() => app.mount("#app"))
});

View File

@@ -12,7 +12,13 @@ export const useLayoutStore = defineStore("layout", {
topNavbar: undefined,
envName: localStorage.getItem("envName") || undefined,
envColor: localStorage.getItem("envColor") || undefined,
sideMenuCollapsed: localStorage.getItem("menuCollapsed") === "true",
sideMenuCollapsed: (() => {
if (typeof window === "undefined") {
return false;
}
return localStorage.getItem("menuCollapsed") === "true" || window.matchMedia("(max-width: 768px)").matches;
})(),
}),
getters: {},
actions: {

View File

@@ -1612,7 +1612,8 @@
"email": "E-Mail",
"firstName": "Vorname",
"lastName": "Nachname",
"password": "Passwort"
"password": "Passwort",
"password_requirements": "Das Passwort muss mindestens 8 Zeichen lang sein und mindestens 1 Großbuchstaben und 1 Zahl enthalten."
},
"login": "Anmelden",
"logout": "Abmelden",

View File

@@ -1477,7 +1477,8 @@
"email": "Email",
"firstName": "First Name",
"lastName": "Last Name",
"password": "Password"
"password": "Password",
"password_requirements": "Password must be at least 8 characters long and include at least 1 uppercase letter and 1 number."
},
"validation": {
"email_required": "Email is required",

View File

@@ -1612,7 +1612,8 @@
"email": "Correo electrónico",
"firstName": "Nombre",
"lastName": "Apellido",
"password": "Contraseña"
"password": "Contraseña",
"password_requirements": "La contraseña debe tener al menos 8 caracteres y contener al menos 1 letra mayúscula y 1 número."
},
"login": "Iniciar sesión",
"logout": "Cerrar sesión",

View File

@@ -1612,7 +1612,8 @@
"email": "E-mail",
"firstName": "Prénom",
"lastName": "Nom de famille",
"password": "Mot de passe"
"password": "Mot de passe",
"password_requirements": "Le mot de passe doit comporter au moins 8 caractères, inclure au moins 1 lettre majuscule et 1 chiffre."
},
"login": "Connexion",
"logout": "Déconnexion",

View File

@@ -1612,7 +1612,8 @@
"email": "ईमेल",
"firstName": "पहला नाम",
"lastName": "अंतिम नाम",
"password": "पासवर्ड"
"password": "पासवर्ड",
"password_requirements": "पासवर्ड कम से कम 8 अक्षरों का होना चाहिए और इसमें कम से कम 1 बड़ा अक्षर और 1 संख्या शामिल होनी चाहिए।"
},
"login": "लॉगिन",
"logout": "लॉगआउट",

View File

@@ -1612,7 +1612,8 @@
"email": "Email",
"firstName": "Nome",
"lastName": "Cognome",
"password": "Password"
"password": "Password",
"password_requirements": "La password deve essere lunga almeno 8 caratteri e includere almeno 1 lettera maiuscola e 1 numero."
},
"login": "Accedi",
"logout": "Logout",

View File

@@ -1612,7 +1612,8 @@
"email": "メール",
"firstName": "名",
"lastName": "姓",
"password": "パスワード"
"password": "パスワード",
"password_requirements": "パスワードは8文字以上で、少なくとも1つの大文字と1つの数字を含める必要があります。"
},
"login": "ログイン",
"logout": "ログアウト",

View File

@@ -1612,7 +1612,8 @@
"email": "이메일",
"firstName": "이름",
"lastName": "성씨",
"password": "비밀번호"
"password": "비밀번호",
"password_requirements": "비밀번호는 최소 8자 이상이어야 하며, 최소 1개의 대문자와 1개의 숫자를 포함해야 합니다."
},
"login": "로그인",
"logout": "로그아웃",

View File

@@ -1612,7 +1612,8 @@
"email": "Email",
"firstName": "Imię",
"lastName": "Nazwisko",
"password": "Hasło"
"password": "Hasło",
"password_requirements": "Hasło musi mieć co najmniej 8 znaków i zawierać co najmniej 1 wielką literę oraz 1 cyfrę."
},
"login": "Zaloguj się",
"logout": "Wyloguj się",

View File

@@ -1612,7 +1612,8 @@
"email": "Email",
"firstName": "Nome",
"lastName": "Sobrenome",
"password": "Senha"
"password": "Senha",
"password_requirements": "A senha deve ter pelo menos 8 caracteres e incluir pelo menos 1 letra maiúscula e 1 número."
},
"login": "Login",
"logout": "Sair",

View File

@@ -1612,7 +1612,8 @@
"email": "Email",
"firstName": "Nome",
"lastName": "Sobrenome",
"password": "Senha"
"password": "Senha",
"password_requirements": "A senha deve ter pelo menos 8 caracteres e incluir pelo menos 1 letra maiúscula e 1 número."
},
"login": "Login",
"logout": "Sair",

View File

@@ -1612,7 +1612,8 @@
"email": "Электронная почта",
"firstName": "Имя",
"lastName": "Фамилия",
"password": "Пароль"
"password": "Пароль",
"password_requirements": "Пароль должен содержать не менее 8 символов, включая как минимум 1 заглавную букву и 1 цифру."
},
"login": "Войти",
"logout": "Выход",

View File

@@ -1612,7 +1612,8 @@
"email": "电子邮件",
"firstName": "名字",
"lastName": "姓氏",
"password": "密码"
"password": "密码",
"password_requirements": "密码必须至少包含8个字符并且至少包含1个大写字母和1个数字。"
},
"login": "登录",
"logout": "注销",