Compare commits

...

6 Commits

Author SHA1 Message Date
Bart Ledoux
14a1a6c104 rename a lot of components 2025-10-31 14:56:50 +01:00
Bart Ledoux
35bce0b5b8 fix: task objectfield 2025-10-31 14:38:10 +01:00
Bart Ledoux
e2b46c4cce remove unused components from task components 2025-10-31 14:33:15 +01:00
Bart Ledoux
49264cb7f9 fix: remove debugging from icons 2025-10-31 14:32:27 +01:00
Bart Ledoux
82e139de03 fix taskDIct loading 2025-10-31 13:15:40 +01:00
Bart Ledoux
7d64692f0f various typescript fixes 2025-10-31 12:06:18 +01:00
43 changed files with 741 additions and 1037 deletions

View File

@@ -97,7 +97,7 @@
import {State} from "@kestra-io/ui-libs" import {State} from "@kestra-io/ui-libs"
import Duration from "../layout/Duration.vue"; import Duration from "../layout/Duration.vue";
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
import FlowUtils from "../../utils/flowUtils"; import * as FlowUtils from "../../utils/flowUtils";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css" import "vue-virtual-scroller/dist/vue-virtual-scroller.css"
import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller"; import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller";
import ChevronRight from "vue-material-design-icons/ChevronRight.vue"; import ChevronRight from "vue-material-design-icons/ChevronRight.vue";

View File

@@ -130,7 +130,7 @@
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
import LogLine from "../logs/LogLine.vue"; import LogLine from "../logs/LogLine.vue";
import Restart from "./Restart.vue"; import Restart from "./Restart.vue";
import LogUtils from "../../utils/logs"; import * as LogUtils from "../../utils/logs";
import Refresh from "vue-material-design-icons/Refresh.vue"; import Refresh from "vue-material-design-icons/Refresh.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {useExecutionsStore} from "../../stores/executions"; import {useExecutionsStore} from "../../stores/executions";

View File

@@ -32,7 +32,7 @@
import permission from "../../models/permission"; import permission from "../../models/permission";
import action from "../../models/action"; import action from "../../models/action";
import {State} from "@kestra-io/ui-libs" import {State} from "@kestra-io/ui-libs"
import FlowUtils from "../../utils/flowUtils"; import * as FlowUtils from "../../utils/flowUtils";
import * as ExecutionUtils from "../../utils/executionUtils"; import * as ExecutionUtils from "../../utils/executionUtils";
import InputsForm from "../../components/inputs/InputsForm.vue"; import InputsForm from "../../components/inputs/InputsForm.vue";
import {inputsToFormData} from "../../utils/submitTask"; import {inputsToFormData} from "../../utils/submitTask";

View File

@@ -190,7 +190,7 @@
import WorkerInfo from "./WorkerInfo.vue"; import WorkerInfo from "./WorkerInfo.vue";
import AiIcon from "../ai/AiIcon.vue"; import AiIcon from "../ai/AiIcon.vue";
import {State} from "@kestra-io/ui-libs" import {State} from "@kestra-io/ui-libs"
import FlowUtils from "../../utils/flowUtils"; import * as FlowUtils from "../../utils/flowUtils";
import _groupBy from "lodash/groupBy"; import _groupBy from "lodash/groupBy";
import {TaskIcon, SECTIONS} from "@kestra-io/ui-libs"; import {TaskIcon, SECTIONS} from "@kestra-io/ui-libs";
import Duration from "../layout/Duration.vue"; import Duration from "../layout/Duration.vue";

View File

@@ -97,13 +97,11 @@
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useVueFlow} from "@vue-flow/core"; import {useVueFlow} from "@vue-flow/core";
// @ts-expect-error no types for SearchField yet
import SearchField from "../layout/SearchField.vue"; import SearchField from "../layout/SearchField.vue";
// @ts-expect-error no types for LogLevelSelector yet // @ts-expect-error no types for LogLevelSelector yet
import LogLevelSelector from "../logs/LogLevelSelector.vue"; import LogLevelSelector from "../logs/LogLevelSelector.vue";
// @ts-expect-error no types for TaskRunDetails yet // @ts-expect-error no types for TaskRunDetails yet
import TaskRunDetails from "../logs/TaskRunDetails.vue"; import TaskRunDetails from "../logs/TaskRunDetails.vue";
// @ts-expect-error no types for Collapse yet
import Collapse from "../layout/Collapse.vue"; import Collapse from "../layout/Collapse.vue";
import Drawer from "../Drawer.vue"; import Drawer from "../Drawer.vue";
import Markdown from "../layout/Markdown.vue"; import Markdown from "../layout/Markdown.vue";

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
class="py-2 line font-monospace" class="py-2 line font-monospace"
:class="{['log-border-' + log.level.toLowerCase()]: cursor && log.level !== undefined, ['key-' + $.vnode.key]: true}" :class="{['log-border-' + log.level.toLowerCase()]: cursor && log.level !== undefined}"
v-if="filtered" v-if="filtered"
:style="logLineStyle" :style="logLineStyle"
> >
@@ -16,7 +16,7 @@
:class="{'d-inline-block': metaWithValue.length === 0, 'me-3': metaWithValue.length === 0}" :class="{'d-inline-block': metaWithValue.length === 0, 'me-3': metaWithValue.length === 0}"
> >
<span class="header-badge text-secondary"> <span class="header-badge text-secondary">
{{ $filters.date(log.timestamp, "iso") }} {{ filters.date(log.timestamp, "iso") }}
</span> </span>
<span v-for="(meta, x) in metaWithValue" :key="x"> <span v-for="(meta, x) in metaWithValue" :key="x">
<span class="header-badge property"> <span class="header-badge property">
@@ -39,64 +39,42 @@
<CopyToClipboard :text="`${log.level} ${log.timestamp} ${log.message}`" link /> <CopyToClipboard :text="`${log.level} ${log.timestamp} ${log.message}`" link />
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import {ref, computed, onMounted, watch, nextTick} from "vue";
import Convert from "ansi-to-html"; import Convert from "ansi-to-html";
import xss from "xss"; import xss from "xss";
import * as Markdown from "../../utils/markdown"; import * as Markdown from "../../utils/markdown";
import MenuRight from "vue-material-design-icons/MenuRight.vue"; import MenuRight from "vue-material-design-icons/MenuRight.vue";
import linkify from "./linkify"; import linkify from "./linkify";
import CopyToClipboard from "../layout/CopyToClipboard.vue"; import CopyToClipboard from "../layout/CopyToClipboard.vue";
import {useStorage} from "@vueuse/core";
import {useRouter} from "vue-router";
import * as filters from "../../utils/filters";
let convert = new Convert(); // Props
const props = defineProps<{
cursor?: boolean,
log: Record<string, any>,
filter?: string,
level?: string,
excludeMetas?: string[],
title?: boolean
}>();
export default { // State
components: { const renderedMarkdown = ref<string | undefined>(undefined);
MenuRight, const logsFontSize = useStorage("logsFontSize", 12);
CopyToClipboard const lineContent = ref<HTMLElement>();
},
props: { const convert = new Convert();
cursor: {
type: Boolean, // Computed
default: false, const logLineStyle = computed(() => ({
}, fontSize: `${logsFontSize.value}px`,
log: { }));
type: Object,
required: true, const metaWithValue = computed(() => {
}, const metaWithValue: any[] = [];
filter: {
type: String,
default: "",
},
level: {
type: String,
default: "INFO",
},
excludeMetas: {
type: Array,
default: () => [],
},
title: {
type: Boolean,
default: false,
},
},
data() {
return {
renderedMarkdown: undefined,
logsFontSize: parseInt(localStorage.getItem("logsFontSize") || "12"),
};
},
async created() {
this.renderedMarkdown = await Markdown.render(this.message, {onlyLink: true, html: true});
},
computed: {
logLineStyle() {
return {
fontSize: `${this.logsFontSize}px`,
};
},
metaWithValue() {
const metaWithValue = [];
const excludes = [ const excludes = [
"message", "message",
"timestamp", "timestamp",
@@ -105,91 +83,88 @@
"level", "level",
"index", "index",
"attemptNumber", "attemptNumber",
"executionKind" "executionKind",
...(props.excludeMetas ?? [])
]; ];
excludes.push.apply(excludes, this.excludeMetas); for (const key in props.log) {
for (const key in this.log) { if (props.log[key] && !excludes.includes(key)) {
if (this.log[key] && !excludes.includes(key)) { let meta: any = {key, value: props.log[key]};
let meta = {key, value: this.log[key]};
if (key === "executionId") { if (key === "executionId") {
meta["router"] = { meta["router"] = {
name: "executions/update", name: "executions/update",
params: { params: {
namespace: this.log["namespace"], namespace: props.log["namespace"],
flowId: this.log["flowId"], flowId: props.log["flowId"],
id: this.log[key], id: props.log[key],
}, },
}; };
} }
if (key === "namespace") { if (key === "namespace") {
meta["router"] = {name: "flows/list", query: {namespace: this.log[key]}}; meta["router"] = {name: "flows/list", query: {namespace: props.log[key]}};
} }
if (key === "flowId") { if (key === "flowId") {
meta["router"] = { meta["router"] = {
name: "flows/update", name: "flows/update",
params: {namespace: this.log["namespace"], id: this.log[key]}, params: {namespace: props.log["namespace"], id: props.log[key]},
}; };
} }
metaWithValue.push(meta); metaWithValue.push(meta);
} }
} }
return metaWithValue; return metaWithValue;
}, });
levelStyle() {
const lowerCaseLevel = this.log?.level?.toLowerCase(); const levelStyle = computed(() => {
const lowerCaseLevel = props.log?.level?.toLowerCase();
return { return {
"border-color": `var(--ks-log-border-${lowerCaseLevel})`, "border-color": `var(--ks-log-border-${lowerCaseLevel})`,
"color": `var(--ks-log-content-${lowerCaseLevel})`, "color": `var(--ks-log-content-${lowerCaseLevel})`,
"background-color": `var(--ks-log-background-${lowerCaseLevel})`, "background-color": `var(--ks-log-background-${lowerCaseLevel})`,
}; };
}, });
filtered() {
return ( const filtered = computed(() =>
this.filter === "" || (this.log.message && this.log.message.toLowerCase().includes(this.filter)) props.filter === "" || (props.log.message && props.log.message.toLowerCase().includes(props.filter ?? ""))
); );
},
iconColor() { const iconColor = computed(() => {
const logLevel = this.log.level?.toLowerCase(); const logLevel = props.log.level?.toLowerCase();
return `var(--ks-log-content-${logLevel}) !important`; // Use CSS variable for icon color return `var(--ks-log-content-${logLevel}) !important`;
}, });
message() {
let logMessage = !this.log.message const message = computed(() => {
let logMessage = !props.log.message
? "" ? ""
: convert.toHtml( : convert.toHtml(
xss(this.log.message, { xss(props.log.message, {
allowList: {span: ["style"]}, allowList: {span: ["style"]},
}) })
); );
logMessage = logMessage.replaceAll( logMessage = logMessage.replaceAll(
/(['"]?)(https?:\/\/[^'"\s]+)(['"]?)/g, /(['"]?)(https?:\/\/[^'"\s]+)(['"]?)/g,
"$1<a href='$2' target='_blank'>$2</a>$3" "$1<a href='$2' target='_blank'>$2</a>$3"
); );
return logMessage; return logMessage;
},
},
mounted() {
window.addEventListener("storage", (event) => {
if (event.key === "logsFontSize") {
this.logsFontSize = parseInt(event.newValue);
}
}); });
const router = useRouter()
onMounted(() => {
setTimeout(() => { setTimeout(() => {
linkify(this.$refs.lineContent, this.$router); linkify(lineContent.value, router);
}, 200); }, 200);
},
watch: {
renderedMarkdown() {
this.$nextTick(() => {
linkify(this.$refs.lineContent, this.$router);
}); });
},
}, watch(renderedMarkdown, () => {
}; nextTick(() => {
linkify(lineContent.value, router);
});
});
// Initial markdown render
(async () => {
renderedMarkdown.value = await Markdown.render(message.value, {onlyLink: true, html: true});
})();
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
div.line { div.line {

View File

@@ -18,7 +18,7 @@
<TaskRunLine <TaskRunLine
:currentTaskRun="currentTaskRun" :currentTaskRun="currentTaskRun"
:followedExecution="followedExecution" :followedExecution="followedExecution"
:flow="flow"
:forcedAttemptNumber="forcedAttemptNumber" :forcedAttemptNumber="forcedAttemptNumber"
:taskRunId="taskRunId" :taskRunId="taskRunId"
:selectedAttemptNumberByTaskRunId="selectedAttemptNumberByTaskRunId" :selectedAttemptNumberByTaskRunId="selectedAttemptNumberByTaskRunId"
@@ -45,7 +45,7 @@
keyField="index" keyField="index"
class="log-lines" class="log-lines"
:class="{'single-line': currentTaskRuns.length === 1}" :class="{'single-line': currentTaskRuns.length === 1}"
:ref="el => logsScrollerRef(el, currentTaskRunIndex, attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id]))" :ref="(el:InstanceType<typeof DynamicScroller>) => logsScrollerRef(el, currentTaskRunIndex, attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id]))"
@resize="scrollToBottomFailedTask" @resize="scrollToBottomFailedTask"
> >
<template #default="{item, index, active}"> <template #default="{item, index, active}">
@@ -82,7 +82,7 @@
:level="level" :level="level"
:log="item" :log="item"
:excludeMetas="excludeMetas" :excludeMetas="excludeMetas"
v-else-if="filter === '' || item.message?.toLowerCase().includes(filter.toLowerCase())" v-else-if="!filter || filter === '' || item.message?.toLowerCase().includes(filter.toLowerCase())"
/> />
<TaskRunDetails <TaskRunDetails
v-if="!taskRunId && isSubflow(currentTaskRun) && shouldDisplaySubflow(index, currentTaskRun) && currentTaskRun.outputs?.executionId" v-if="!taskRunId && isSubflow(currentTaskRun) && shouldDisplaySubflow(index, currentTaskRun) && currentTaskRun.outputs?.executionId"
@@ -109,561 +109,452 @@
</DynamicScroller> </DynamicScroller>
</template> </template>
<script setup> <script setup lang="ts">
import {ref, computed, watch, onMounted, onBeforeUnmount, nextTick, useTemplateRef} from "vue";
import Download from "vue-material-design-icons/Download.vue"; import Download from "vue-material-design-icons/Download.vue";
</script>
<script>
import LogLine from "./LogLine.vue"; import LogLine from "./LogLine.vue";
import {State} from "@kestra-io/ui-libs" import {State} from "@kestra-io/ui-libs";
import _xor from "lodash/xor"; import _xor from "lodash/xor";
import _groupBy from "lodash/groupBy"; import _groupBy from "lodash/groupBy";
import moment from "moment"; import moment from "moment";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css" import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import {logDisplayTypes} from "../../utils/constants"; import {logDisplayTypes} from "../../utils/constants";
// @ts-expect-error no types for dynamic scroller
import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller"; import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller";
import {mapStores} from "pinia";
import {useCoreStore} from "../../stores/core"; import {useCoreStore} from "../../stores/core";
import {useExecutionsStore} from "../../stores/executions"; import {useExecutionsStore} from "../../stores/executions";
import ForEachStatus from "../executions/ForEachStatus.vue"; import ForEachStatus from "../executions/ForEachStatus.vue";
// @ts-expect-error no types for TaskRunLine
import TaskRunLine from "../executions/TaskRunLine.vue"; import TaskRunLine from "../executions/TaskRunLine.vue";
import FlowUtils from "../../utils/flowUtils"; import * as FlowUtils from "../../utils/flowUtils";
import FilePreview from "../executions/FilePreview.vue"; import FilePreview from "../executions/FilePreview.vue";
import {apiUrl} from "override/utils/route"; import {apiUrl} from "override/utils/route";
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
import LogUtils from "../../utils/logs"; import * as LogUtils from "../../utils/logs";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
import {useAxios} from "../../utils/axios";
import {useI18n} from "vue-i18n";
const {t} = useI18n();
const props = defineProps<{
logCursor?: string,
levelToHighlight?: string,
level?: string,
filter?: string,
taskRunId?: string,
excludeMetas?: string[],
forcedAttemptNumber?: number,
targetExecutionId?: string,
targetFlow?: any,
allowAutoExpandSubflows?: boolean,
showProgressBar?: boolean,
showLogs?: boolean
}>();
export default { const emit = defineEmits([
name: "TaskRunDetails", "opened-taskruns-count",
components: { "follow",
FilePreview, "reset-expand-collapse-all-switch",
TaskRunLine, "log-cursor",
ForEachStatus, "log-indices-by-level"
LogLine, ]);
DynamicScroller,
DynamicScrollerItem, const coreStore = useCoreStore();
}, const executionsStore = useExecutionsStore();
emits: ["opened-taskruns-count", "follow", "reset-expand-collapse-all-switch", "log-cursor", "log-indices-by-level"],
props: { const shownAttemptsUid = ref<string[]>([]);
logCursor: { const rawLogs = ref<any[]>([]);
type: String, const timer = ref<any>(undefined);
default: undefined, const timeout = ref<any>(undefined);
}, const selectedAttemptNumberByTaskRunId = ref<Record<string, number>>({});
levelToHighlight: { const executionSSE = ref<any>(undefined);
type: String, const logsSSE = ref<any>(undefined);
default: undefined const flow = ref<any>(undefined);
}, const logsBuffer = ref<any[]>([]);
level: { const shownSubflowsIds = ref<any[]>([]);
type: String, const logFileSizeByPath = ref<Record<string, any>>({});
default: "INFO", const childrenLogIndicesByLevelByChildUid = ref<Record<string, any>>({});
}, const logsScrollerRefs = ref<Record<string, any>>({});
filter: { const subflowTaskRunDetailsRefs = ref<Record<string, any>>({});
type: String, const throttledExecutionUpdate = ref<any>(undefined);
default: "", const targetExecution = ref<any>(undefined);
},
taskRunId: { // Computed
type: String, const followedExecution = computed(() =>
default: undefined, props.targetExecutionId === undefined ? executionsStore.execution : targetExecution.value
}, );
excludeMetas: {
type: Array, const currentTaskRuns = computed(() =>
default: () => [], followedExecution.value?.taskRunList?.filter((tr: any) => props.taskRunId ? tr.id === props.taskRunId : true) ?? []
}, );
forcedAttemptNumber: {
type: Number, const taskRunById = computed(() =>
default: undefined Object.fromEntries(currentTaskRuns.value.map((taskRun: any) => [taskRun.id, taskRun]))
}, );
// allows to fetch the execution at startup
targetExecutionId: { const logsWithIndexByAttemptUid = computed(() => {
type: String, const logFilesWrappers = currentTaskRuns.value.flatMap((taskRun: any) =>
default: undefined attempts(taskRun)
}, .filter((attempt: any) => attempt.logFile !== undefined)
// allows to pass directly a flow source (since it is already fetched by parent component) .map((attempt: any, attemptNumber: number) => ({logFile: attempt.logFile, taskRunId: taskRun.id, attemptNumber}))
targetFlow: { );
type: Object, logFilesWrappers.forEach((logFileWrapper: any) => fetchAndStoreLogFileSize(logFileWrapper.logFile));
default: undefined const indexedLogs = [...filteredLogs.value, ...logFilesWrappers]
}, .filter((logLine: any) => logLine.logFile !== undefined || (props.filter === "" || logLine?.message?.toLowerCase().includes(props.filter?.toLowerCase() ?? "") || isSubflow(taskRunById.value[logLine.taskRunId])))
allowAutoExpandSubflows: { .map((logLine: any, index: number) => ({...logLine, index}));
type: Boolean, return _groupBy(indexedLogs, (indexedLog: any) => attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
default: true });
},
showProgressBar: { const autoExpandTaskRunStates = computed(() => {
type: Boolean, switch (localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT) {
default: true case logDisplayTypes.ERROR:
}, return [State.FAILED, State.RUNNING, State.PAUSED];
showLogs: { case logDisplayTypes.ALL:
type: Boolean, return State.arrayAllStates().map((s: any) => s.name);
default: true case logDisplayTypes.HIDDEN:
return [];
default:
return State.arrayAllStates().map((s: any) => s.name);
} }
}, });
data() {
return { const taskTypeAndTaskRunByTaskId = computed(() =>
showOutputs: {}, Object.fromEntries(followedExecution.value?.taskRunList?.map((taskRun: any) => [taskRun.taskId, [taskType(taskRun), taskRun]]))
showMetrics: {}, );
fullscreen: false,
followed: false, const forEachItemExecutableByRootTaskId = computed(() =>
shownAttemptsUid: [], Object.fromEntries(
rawLogs: [], Object.entries(taskTypeAndTaskRunByTaskId.value)
timer: undefined, .filter(([, taskTypeAndTaskRun]: any) => taskTypeAndTaskRun[0] === "io.kestra.plugin.core.flow.ForEachItem" || taskTypeAndTaskRun[0] === "io.kestra.core.tasks.flows.ForEachItem")
timeout: undefined, .map(([taskId]: any) => [taskId, taskTypeAndTaskRunByTaskId.value?.[taskId + "_items"]?.[1]])
selectedAttemptNumberByTaskRunId: {}, )
executionSSE: undefined, );
logsSSE: undefined,
flow: undefined, const currentTaskRunsLogIndicesByLevel = computed(() =>
logsBuffer: [], currentTaskRuns.value.reduce((currentTaskRunsLogIndicesByLevel: any, taskRun: any, taskRunIndex: number) => {
shownSubflowsIds: [], if (shouldDisplayLogs(taskRun)) {
logFileSizeByPath: {}, const currentTaskRunLogs = logsWithIndexByAttemptUid.value[attemptUid(taskRun.id, selectedAttemptNumberByTaskRunId.value[taskRun.id])];
selectedLogLevel: undefined, currentTaskRunLogs?.forEach((log: any, logIndex: number) => {
childrenLogIndicesByLevelByChildUid: {}, currentTaskRunsLogIndicesByLevel[log.level] = [...(currentTaskRunsLogIndicesByLevel?.[log.level] ?? []), taskRunIndex + "/" + logIndex];
logsScrollerRefs: {}, });
subflowTaskRunDetailsRefs: {}, }
throttledExecutionUpdate: undefined, return currentTaskRunsLogIndicesByLevel;
targetExecution: undefined }, {})
}; );
},
watch: { const allLogIndicesByLevel = computed(() => {
"shownAttemptsUid.length": function (openedTaskrunsCount) { const currentTaskRunsLogIndicesByLevelLocal = {...currentTaskRunsLogIndicesByLevel.value};
this.$emit("opened-taskruns-count", openedTaskrunsCount); return Object.entries(childrenLogIndicesByLevelByChildUid.value).reduce((allLogIndicesByLevel: any, [logUid, childrenLogIndicesByLevel]: any) => {
}, Object.entries(childrenLogIndicesByLevel).forEach(([level, logIndices]: any) => {
level: function () { allLogIndicesByLevel[level] = [...(allLogIndicesByLevel?.[level] ?? []), ...logIndices.map((logIndex: any) => logUid + "/" + logIndex)];
this.rawLogs = []; });
this.loadLogs(this.followedExecution.id); return allLogIndicesByLevel;
}, }, currentTaskRunsLogIndicesByLevelLocal);
currentTaskRuns: { });
handler(taskRuns) {
// by default we preselect the last attempt for each task run const levelOrLower = computed(() => LogUtils.levelOrLower(props.level ?? "INFO"));
this.selectedAttemptNumberByTaskRunId = Object.fromEntries(taskRuns.map(taskRun => [taskRun.id, this.forcedAttemptNumber ?? this.attempts(taskRun).length - 1]));
this.autoExpandBasedOnSettings(); const filteredLogs = computed(() =>
}, rawLogs.value.filter((log: any) => levelOrLower.value.includes(log.level))
immediate: true, );
deep: true
}, // Watchers
targetFlow: { watch(() => shownAttemptsUid.value.length, (openedTaskrunsCount) => {
handler: function (flowSource) { emit("opened-taskruns-count", openedTaskrunsCount);
});
watch(() => props.level, () => {
rawLogs.value = [];
loadLogs(followedExecution.value.id);
});
watch(currentTaskRuns, (taskRuns) => {
selectedAttemptNumberByTaskRunId.value = Object.fromEntries(taskRuns.map((taskRun: any) => [taskRun.id, props.forcedAttemptNumber ?? attempts(taskRun).length - 1]));
autoExpandBasedOnSettings();
}, {immediate: true, deep: true});
watch(() => props.targetFlow, (flowSource) => {
if (flowSource) { if (flowSource) {
this.flow = flowSource; flow.value = flowSource;
}
},
immediate: true
},
followedExecution: {
handler: async function (newExecution, oldExecution) {
if (!newExecution) {
return;
} }
}, {immediate: true});
const taskRunScroller = useTemplateRef("taskRunScroller");
watch(followedExecution, async (newExecution, oldExecution) => {
if (!newExecution) return;
if (!oldExecution) { if (!oldExecution) {
this.$nextTick(() => { nextTick(() => {
const parentScroller = this.$refs.taskRunScroller?.$el?.parentNode?.closest(".vue-recycle-scroller"); const parentScroller = (taskRunScroller.value as any)?.$el?.parentNode?.closest(".vue-recycle-scroller");
if (parentScroller) { if (parentScroller) {
const scrollerStyles = window.getComputedStyle(parentScroller); const scrollerStyles = window.getComputedStyle(parentScroller);
this.$refs.taskRunScroller.$el.style.maxHeight = `${scrollerStyles.getPropertyValue("max-height") - parentScroller.clientHeight}px`; (taskRunScroller.value as any).$el.style.maxHeight = `${Number(scrollerStyles.getPropertyValue("max-height")) - parentScroller.clientHeight}px`;
} }
}) });
} }
if (!props.targetFlow) {
if (!this.targetFlow) { flow.value = await executionsStore.loadFlowForExecution({
this.flow = await this.executionsStore.loadFlowForExecution(
{
namespace: newExecution.namespace, namespace: newExecution.namespace,
flowId: newExecution.flowId, flowId: newExecution.flowId,
revision: newExecution.flowRevision, revision: newExecution.flowRevision,
store: false store: false
});
} }
); if (!State.isRunning(followedExecution.value.state.current)) {
setTimeout(() => closeLogsSSE(), 2000);
if (!logsSSE.value) {
loadLogs(newExecution.id);
} }
if (!State.isRunning(this.followedExecution.state.current)) {
// wait a bit to make sure we don't miss logs as log indexer is asynchronous
setTimeout(() => {
this.closeLogsSSE()
}, 2000);
if (!this.logsSSE) {
this.loadLogs(newExecution.id);
}
return; return;
} }
if (!logsSSE.value) {
followLogs(newExecution.id);
}
}, {immediate: true});
// running or paused watch(allLogIndicesByLevel, () => {
if (!this.logsSSE) { emit("log-indices-by-level", allLogIndicesByLevel.value);
this.followLogs(newExecution.id); });
}
}, watch(() => props.logCursor, (newValue) => {
immediate: true
},
allLogIndicesByLevel() {
this.$emit("log-indices-by-level", this.allLogIndicesByLevel);
},
logCursor(newValue) {
if (newValue !== undefined) { if (newValue !== undefined) {
this.scrollToLog(newValue); scrollToLog(newValue);
} }
} });
},
mounted() { // Lifecycle
this.throttledExecutionUpdate = throttle((executionEvent) => { onMounted(() => {
this.targetExecution = JSON.parse(executionEvent.data); throttledExecutionUpdate.value = throttle((executionEvent: any) => {
targetExecution.value = JSON.parse(executionEvent.data);
}, 500); }, 500);
if (this.targetExecutionId) { if (props.targetExecutionId) {
this.followExecution(this.targetExecutionId); followExecution(props.targetExecutionId);
}
autoExpandBasedOnSettings();
});
onBeforeUnmount(() => {
closeLogsSSE();
});
// Methods
function fileUrl(path: string) {
return `${apiUrl()}/executions/${followedExecution.value.id}/file?path=${path}`;
} }
this.autoExpandBasedOnSettings(); const axios = useAxios();
},
computed: {
...mapStores(useCoreStore, useExecutionsStore),
followedExecution() {
return this.targetExecutionId === undefined ? this.executionsStore.execution : this.targetExecution;
},
Download() {
return Download
},
currentTaskRuns() {
return this.followedExecution?.taskRunList?.filter(tr => this.taskRunId ? tr.id === this.taskRunId : true) ?? [];
},
params() {
let params = {minLevel: this.level};
if (this.taskRunId) { async function fetchAndStoreLogFileSize(path: string) {
params.taskRunId = this.taskRunId; if (logFileSizeByPath.value[path] !== undefined) return;
const axiosResponse = await axios.get(`${apiUrl()}/executions/${followedExecution.value.id}/file/metas?path=${path}`, {
validateStatus: (status: number) => status === 200 || status === 404 || status === 422
});
logFileSizeByPath.value[path] = Utils.humanFileSize(axiosResponse.data.size);
}
if (this.forcedAttemptNumber) { function closeLogsSSE() {
params.attempt = this.forcedAttemptNumber; if (logsSSE.value) {
logsSSE.value.close();
logsSSE.value = undefined;
} }
} }
return params function autoExpandBasedOnSettings() {
}, if (autoExpandTaskRunStates.value.length === 0) return;
taskRunById() { if (followedExecution.value === undefined) {
return Object.fromEntries(this.currentTaskRuns.map(taskRun => [taskRun.id, taskRun])); setTimeout(() => autoExpandBasedOnSettings(), 50);
}, return;
logsWithIndexByAttemptUid() { }
const logFilesWrappers = this.currentTaskRuns.flatMap(taskRun => currentTaskRuns.value.forEach((taskRun: any) => {
this.attempts(taskRun) if (isSubflow(taskRun) && props.allowAutoExpandSubflows === false) return;
.filter(attempt => attempt.logFile !== undefined) if (props.taskRunId === taskRun.id || autoExpandTaskRunStates.value.includes(taskRun.state.current)) {
.map((attempt, attemptNumber) => ({logFile: attempt.logFile, taskRunId: taskRun.id, attemptNumber})) showAttempt(attemptUid(taskRun.id, selectedAttemptNumberByTaskRunId.value[taskRun.id]));
);
logFilesWrappers.forEach(logFileWrapper => this.fetchAndStoreLogFileSize(logFileWrapper.logFile))
const indexedLogs = [...this.filteredLogs, ...logFilesWrappers]
.filter(logLine => logLine.logFile !== undefined || (this.filter === "" || logLine?.message.toLowerCase().includes(this.filter.toLowerCase()) || this.isSubflow(this.taskRunById[logLine.taskRunId])))
.map((logLine, index) => ({...logLine, index}));
return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
},
autoExpandTaskRunStates() {
switch (localStorage.getItem("logDisplay") || logDisplayTypes.DEFAULT) {
case logDisplayTypes.ERROR:
return [State.FAILED, State.RUNNING, State.PAUSED]
case logDisplayTypes.ALL:
return State.arrayAllStates().map(s => s.name)
case logDisplayTypes.HIDDEN:
return []
default:
return State.arrayAllStates().map(s => s.name)
} }
},
taskTypeAndTaskRunByTaskId() {
return Object.fromEntries(this.followedExecution?.taskRunList?.map(taskRun => [taskRun.taskId, [this.taskType(taskRun), taskRun]]));
},
forEachItemExecutableByRootTaskId() {
return Object.fromEntries(
Object.entries(this.taskTypeAndTaskRunByTaskId)
.filter(([, taskTypeAndTaskRun]) => taskTypeAndTaskRun[0] === "io.kestra.plugin.core.flow.ForEachItem" || taskTypeAndTaskRun[0] === "io.kestra.core.tasks.flows.ForEachItem")
.map(([taskId]) => [taskId, this.taskTypeAndTaskRunByTaskId?.[taskId + "_items"]?.[1]])
);
},
currentTaskRunsLogIndicesByLevel() {
return this.currentTaskRuns.reduce((currentTaskRunsLogIndicesByLevel, taskRun, taskRunIndex) => {
if (this.shouldDisplayLogs(taskRun)) {
const currentTaskRunLogs = this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])];
currentTaskRunLogs?.forEach((log, logIndex) => {
currentTaskRunsLogIndicesByLevel[log.level] = [...(currentTaskRunsLogIndicesByLevel?.[log.level] ?? []), taskRunIndex + "/" + logIndex];
}); });
} }
return currentTaskRunsLogIndicesByLevel function shouldDisplayProgressBar(taskRun: any) {
}, {}); return props.showProgressBar &&
}, (taskType(taskRun) === "io.kestra.plugin.core.flow.ForEachItem" || taskType(taskRun) === "io.kestra.core.tasks.flows.ForEachItem") &&
allLogIndicesByLevel() { forEachItemExecutableByRootTaskId.value[taskRun.taskId]?.outputs?.iterations !== undefined &&
const currentTaskRunsLogIndicesByLevel = {...this.currentTaskRunsLogIndicesByLevel}; forEachItemExecutableByRootTaskId.value[taskRun.taskId]?.outputs?.numberOfBatches !== undefined;
return Object.entries(this.childrenLogIndicesByLevelByChildUid).reduce((allLogIndicesByLevel, [logUid, childrenLogIndicesByLevel]) => {
Object.entries(childrenLogIndicesByLevel).forEach(([level, logIndices]) => {
allLogIndicesByLevel[level] = [...(allLogIndicesByLevel?.[level] ?? []), ...logIndices.map(logIndex => logUid + "/" + logIndex)];
});
return allLogIndicesByLevel;
}, currentTaskRunsLogIndicesByLevel);
},
levelOrLower() {
return LogUtils.levelOrLower(this.level);
},
filteredLogs() {
return this.rawLogs.filter(log => this.levelOrLower.includes(log.level));
}
},
methods: {
fileUrl(path) {
return `${apiUrl()}/executions/${this.followedExecution.id}/file?path=${path}`;
},
async fetchAndStoreLogFileSize(path){
if (this.logFileSizeByPath[path] !== undefined) {
return;
} }
const axiosResponse = await this.$http(`${apiUrl()}/executions/${this.followedExecution.id}/file/metas?path=${path}`, { function shouldDisplayLogs(taskRun: any) {
validateStatus: (status) => status === 200 || status === 404 || status === 422 return (props.taskRunId ||
}); (shownAttemptsUid.value.includes(attemptUid(taskRun.id, selectedAttemptNumberByTaskRunId.value[taskRun.id])) &&
this.logFileSizeByPath[path] = Utils.humanFileSize(axiosResponse.data.size); logsWithIndexByAttemptUid.value[attemptUid(taskRun.id, selectedAttemptNumberByTaskRunId.value[taskRun.id])])) &&
}, props.showLogs;
closeLogsSSE() {
if (this.logsSSE) {
this.logsSSE.close();
this.logsSSE = undefined;
}
},
toggleExpandCollapseAll() {
if(this.shownAttemptsUid.length === 0){
this.expandAll()
} else {
this.collapseAll()
}
},
autoExpandBasedOnSettings() {
if (this.autoExpandTaskRunStates.length === 0) {
return;
} }
if (this.followedExecution === undefined) { function closeTargetExecutionSSE() {
setTimeout(() => this.autoExpandBasedOnSettings(), 50); if (executionSSE.value) {
return; executionSSE.value.close();
executionSSE.value = undefined;
} }
this.currentTaskRuns.forEach((taskRun) => {
if (this.isSubflow(taskRun) && !this.allowAutoExpandSubflows) {
return;
} }
if (this.taskRunId === taskRun.id || this.autoExpandTaskRunStates.includes(taskRun.state.current)) { function followExecution(executionId: string) {
this.showAttempt(this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])); closeTargetExecutionSSE();
} executionsStore
}); .followExecution({id: executionId, rawSSE: true}, t)
}, .then((sse: any) => {
shouldDisplayProgressBar(taskRun) { executionSSE.value = sse;
return this.showProgressBar && executionSSE.value.onmessage = (executionEvent: any) => {
(this.taskType(taskRun) === "io.kestra.plugin.core.flow.ForEachItem" || this.taskType(taskRun) === "io.kestra.core.tasks.flows.ForEachItem") &&
this.forEachItemExecutableByRootTaskId[taskRun.taskId]?.outputs?.iterations !== undefined &&
this.forEachItemExecutableByRootTaskId[taskRun.taskId]?.outputs?.numberOfBatches !== undefined;
},
shouldDisplayLogs(taskRun) {
return (this.taskRunId ||
(this.shownAttemptsUid.includes(this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])) &&
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
this.showLogs
},
closeTargetExecutionSSE() {
if (this.executionSSE) {
this.executionSSE.close();
this.executionSSE = undefined;
}
},
followExecution(executionId) {
this.closeTargetExecutionSSE();
this.executionsStore
.followExecution({id: executionId, rawSSE: true})
.then(sse => {
this.executionSSE = sse;
this.executionSSE.onmessage = executionEvent => {
const isEnd = executionEvent && executionEvent.lastEventId === "end"; const isEnd = executionEvent && executionEvent.lastEventId === "end";
// we are receiving a first "fake" event to force initializing the connection: ignoring it
if (executionEvent.lastEventId !== "start") { if (executionEvent.lastEventId !== "start") {
this.throttledExecutionUpdate(executionEvent); throttledExecutionUpdate.value(executionEvent);
} }
if (isEnd) { if (isEnd) {
this.closeTargetExecutionSSE(); closeTargetExecutionSSE();
this.throttledExecutionUpdate.flush(); throttledExecutionUpdate.value.flush();
} }
}
});
},
followLogs(executionId) {
this.executionsStore
.followLogs({id: executionId})
.then(sse => {
this.logsSSE = sse;
this.logsSSE.onmessage = event => {
// we are receiving a first "fake" event to force initializing the connection: ignoring it
if (event.lastEventId !== "start") {
this.logsBuffer = this.logsBuffer.concat(JSON.parse(event.data));
}
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.timer = moment()
this.rawLogs = this.rawLogs.concat(this.logsBuffer);
this.logsBuffer = [];
this.scrollToBottomFailedTask();
}, 100);
// force at least 1 logs refresh / 500ms
if (moment().diff(this.timer, "seconds") > 0.5) {
clearTimeout(this.timeout);
this.timer = moment()
this.rawLogs = this.rawLogs.concat(this.logsBuffer);
this.logsBuffer = [];
this.scrollToBottomFailedTask();
}
}
this.logsSSE.onerror = _ => {
this.coreStore.message = {
variant: "error",
title: this.$t("error"),
message: this.$t("something_went_wrong.loading_execution"),
}; };
});
} }
})
}, function followLogs(executionId: string) {
isSubflow(taskRun) { executionsStore
.followLogs({id: executionId})
.then((sse: any) => {
logsSSE.value = sse;
logsSSE.value.onmessage = (event: any) => {
if (event.lastEventId !== "start") {
logsBuffer.value = logsBuffer.value.concat(JSON.parse(event.data));
}
clearTimeout(timeout.value);
timeout.value = setTimeout(() => {
timer.value = moment();
rawLogs.value = rawLogs.value.concat(logsBuffer.value);
logsBuffer.value = [];
scrollToBottomFailedTask();
}, 100);
if (moment().diff(timer.value, "seconds") > 0.5) {
clearTimeout(timeout.value);
timer.value = moment();
rawLogs.value = rawLogs.value.concat(logsBuffer.value);
logsBuffer.value = [];
scrollToBottomFailedTask();
}
};
logsSSE.value.onerror = (_: any) => {
coreStore.message = {
variant: "error",
title: t("error"),
message: t("something_went_wrong.loading_execution"),
};
};
});
}
function isSubflow(taskRun: any) {
return taskRun.outputs?.executionId; return taskRun.outputs?.executionId;
}, }
shouldDisplaySubflow(taskRunIndex, taskRun) { function shouldDisplaySubflow(taskRunIndex: number, taskRun: any) {
const subflowExecutionId = taskRun.outputs.executionId; const subflowExecutionId = taskRun.outputs.executionId;
const index = this.shownSubflowsIds.findIndex(item => item.subflowExecutionId === subflowExecutionId) const index = shownSubflowsIds.value.findIndex(item => item.subflowExecutionId === subflowExecutionId);
if (index === -1) { if (index === -1) {
this.shownSubflowsIds.push({subflowExecutionId: subflowExecutionId, taskRunIndex: taskRunIndex}); shownSubflowsIds.value.push({subflowExecutionId: subflowExecutionId, taskRunIndex: taskRunIndex});
return true; return true;
} else { } else {
return this.shownSubflowsIds[index].taskRunIndex === taskRunIndex; return shownSubflowsIds.value[index].taskRunIndex === taskRunIndex;
} }
},
expandAll() {
if (!this.followedExecution) {
setTimeout(() => this.expandAll(), 50);
return;
} }
this.shownAttemptsUid = this.currentTaskRuns.map(taskRun => this.attemptUid( function attemptUid(taskRunId: string, attemptNumber: number) {
taskRun.id, return `${taskRunId}-${attemptNumber}`;
this.selectedAttemptNumberByTaskRunId[taskRun.id] ?? 0
));
this.shownAttemptsUid.forEach(attemptUid => this.logsScrollerRefs?.[attemptUid]?.[0]?.scrollToBottom());
this.expandSubflows();
},
expandSubflows() {
if (this.currentTaskRuns.some(taskRun => this.isSubflow(taskRun))) {
const subflowLogsElements = Object.values(this.subflowTaskRunDetailsRefs);
if (subflowLogsElements.length === 0) {
setTimeout(() => this.expandSubflows(), 50);
} }
subflowLogsElements?.forEach(subflowLogs => subflowLogs.expandAll()); function scrollToBottomFailedTask() {
} if (autoExpandTaskRunStates.value.includes(followedExecution.value?.state?.current)) {
}, currentTaskRuns.value.forEach((taskRun: any) => {
collapseAll() {
this.shownAttemptsUid = [];
},
attemptUid(taskRunId, attemptNumber) {
return `${taskRunId}-${attemptNumber}`
},
scrollToBottomFailedTask() {
if (this.autoExpandTaskRunStates.includes(this.followedExecution?.state?.current)) {
this.currentTaskRuns.forEach((taskRun) => {
if (taskRun.state.current === State.FAILED || taskRun.state.current === State.RUNNING) { if (taskRun.state.current === State.FAILED || taskRun.state.current === State.RUNNING) {
const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : (this.forcedAttemptNumber ?? 0) const attemptNumber = taskRun.attempts ? taskRun.attempts.length - 1 : (props.forcedAttemptNumber ?? 0);
if (this.shownAttemptsUid.includes(`${taskRun.id}-${attemptNumber}`)) { if (shownAttemptsUid.value.includes(`${taskRun.id}-${attemptNumber}`)) {
this.logsScrollerRefs?.[`${taskRun.id}-${attemptNumber}`]?.scrollToBottom(); logsScrollerRefs.value?.[`${taskRun.id}-${attemptNumber}`]?.scrollToBottom();
} }
} }
}); });
} }
},
uniqueTaskRunDisplayFilter(currentTaskRun) {
return !(this.taskRunId && this.taskRunId !== currentTaskRun.id);
},
loadLogs(executionId) {
if (!this.showLogs) {
return;
} }
this.executionsStore.loadLogs({ function uniqueTaskRunDisplayFilter(currentTaskRun: any) {
return !(props.taskRunId && props.taskRunId !== currentTaskRun.id);
}
function loadLogs(executionId: string) {
if (!props.showLogs) return;
executionsStore.loadLogs({
executionId, executionId,
params: { params: {
minLevel: this.level minLevel: props.level
} }
}).then(logs => { }).then((logs: any) => {
this.rawLogs = logs rawLogs.value = logs;
}); });
}, }
attempts(taskRun) {
if (this.followedExecution.state.current === State.RUNNING || this.forcedAttemptNumber === undefined) { function attempts(taskRun: any) {
if (followedExecution.value.state.current === State.RUNNING || props.forcedAttemptNumber === undefined) {
return taskRun.attempts ?? [{state: taskRun.state}]; return taskRun.attempts ?? [{state: taskRun.state}];
} }
return taskRun.attempts ? [taskRun.attempts[props.forcedAttemptNumber]] : [];
return taskRun.attempts ? [taskRun.attempts[this.forcedAttemptNumber]] : [];
},
showAttempt(attemptUid) {
if (!this.shownAttemptsUid.includes(attemptUid)) {
this.shownAttemptsUid.push(attemptUid);
} }
},
toggleShowAttempt(attemptUid) { function showAttempt(attemptUidVal: string) {
this.shownAttemptsUid = _xor(this.shownAttemptsUid, [attemptUid]) if (!shownAttemptsUid.value.includes(attemptUidVal)) {
}, shownAttemptsUid.value.push(attemptUidVal);
swapDisplayedAttempt(event) { }
}
function toggleShowAttempt(attemptUidVal: string) {
shownAttemptsUid.value = _xor(shownAttemptsUid.value, [attemptUidVal]);
}
function swapDisplayedAttempt(event: any) {
const {taskRunId, attemptNumber: newDisplayedAttemptNumber} = event; const {taskRunId, attemptNumber: newDisplayedAttemptNumber} = event;
this.shownAttemptsUid = this.shownAttemptsUid.map(attemptUid => attemptUid.startsWith(`${taskRunId}-`) shownAttemptsUid.value = shownAttemptsUid.value.map(attemptUidVal => attemptUidVal.startsWith(`${taskRunId}-`)
? this.attemptUid(taskRunId, newDisplayedAttemptNumber) ? attemptUid(taskRunId, newDisplayedAttemptNumber)
: attemptUid : attemptUidVal
); );
selectedAttemptNumberByTaskRunId.value[taskRunId] = newDisplayedAttemptNumber;
}
this.selectedAttemptNumberByTaskRunId[taskRunId] = newDisplayedAttemptNumber; function taskType(taskRun: any) {
},
taskType(taskRun) {
if (!taskRun) return undefined; if (!taskRun) return undefined;
const task = FlowUtils.findTaskById(flow.value, taskRun?.taskId);
const task = FlowUtils.findTaskById(this.flow, taskRun?.taskId);
const parentTaskRunId = taskRun.parentTaskRunId; const parentTaskRunId = taskRun.parentTaskRunId;
if (task === undefined && parentTaskRunId) { if (task === undefined && parentTaskRunId) {
return this.taskType(this.taskRunById[parentTaskRunId]) return taskType(taskRunById.value[parentTaskRunId]);
} }
return task ? task.type : undefined; return task ? task.type : undefined;
}, }
emitLogCursor(logCursor) {
this.$emit("log-cursor", logCursor); function emitLogCursor(logCursor: string) {
}, emit("log-cursor", logCursor);
childLogIndicesByLevel(taskRunIndex, logIndex, logIndicesByLevel) { }
this.childrenLogIndicesByLevelByChildUid[`${taskRunIndex}/${logIndex}`] = logIndicesByLevel;
}, function childLogIndicesByLevel(taskRunIndex: number, logIndex: number, logIndicesByLevel: any) {
logsScrollerRef(el, ...ids) { childrenLogIndicesByLevelByChildUid.value[`${taskRunIndex}/${logIndex}`] = logIndicesByLevel;
ids.forEach(id => this.logsScrollerRefs[id] = el); }
},
subflowTaskRunDetailsRef(el, id) { function logsScrollerRef(el: any, ...ids: string[]) {
this.subflowTaskRunDetailsRefs[id] = el; ids.forEach(id => logsScrollerRefs.value[id] = el);
}, }
scrollToLog(logId) {
function subflowTaskRunDetailsRef(el: any, id: string) {
subflowTaskRunDetailsRefs.value[id] = el;
}
function scrollToLog(logId: string) {
const split = logId.split("/"); const split = logId.split("/");
this.$refs.taskRunScroller.scrollToItem(split[0]); (taskRunScroller.value as any).scrollToItem(split[0]);
this.logsScrollerRefs?.[split[0]]?.scrollToItem(split[1]); logsScrollerRefs.value?.[split[0]]?.scrollToItem(split[1]);
if (split.length > 2) { if (split.length > 2) {
this.subflowTaskRunDetailsRefs?.[split[0] + "/" + split[1]]?.scrollToLog(split.slice(2).join("/")); subflowTaskRunDetailsRefs.value?.[split[0] + "/" + split[1]]?.scrollToLog(split.slice(2).join("/"));
} }
} }
},
beforeUnmount() {
this.closeLogsSSE()
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@kestra-io/ui-libs/src/scss/variables"; @import "@kestra-io/ui-libs/src/scss/variables";

View File

@@ -10,7 +10,7 @@
<template #tasks> <template #tasks>
<TaskObjectField <TaskObjectField
v-bind="v" v-bind="v"
@update:model-value="(val) => onTaskUpdateField(v.fieldKey, val)" @update:model-value="(val: any) => onTaskUpdateField(v.fieldKey, val)"
/> />
</template> </template>
</Wrapper> </Wrapper>

View File

@@ -18,7 +18,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div @click="isPlugin && pluginsStore.updateDocumentation(taskObject as Parameters<typeof pluginsStore.updateDocumentation>[0])"> <div @click="isPlugin && pluginsStore.updateDocumentation(taskObject as Parameters<typeof pluginsStore.updateDocumentation>[0])">
<TaskObject <BlockObject
v-loading="isLoading" v-loading="isLoading"
v-if="(selectedTaskType || !isTaskDefinitionBasedOnType) && schema" v-if="(selectedTaskType || !isTaskDefinitionBasedOnType) && schema"
name="root" name="root"
@@ -26,6 +26,7 @@
@update:model-value="onTaskInput" @update:model-value="onTaskInput"
:schema :schema
:properties :properties
root=""
/> />
</div> </div>
</template> </template>
@@ -34,7 +35,6 @@
import {computed, inject, onActivated, provide, ref, toRaw, watch} from "vue"; import {computed, inject, onActivated, provide, ref, toRaw, watch} from "vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils"; import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import TaskObject from "./tasks/TaskObject.vue";
import PluginSelect from "../../plugins/PluginSelect.vue"; import PluginSelect from "../../plugins/PluginSelect.vue";
import {NoCodeElement, Schemas} from "../utils/types"; import {NoCodeElement, Schemas} from "../utils/types";
import { import {
@@ -50,6 +50,7 @@
import {getValueAtJsonPath, resolve$ref} from "../../../utils/utils"; import {getValueAtJsonPath, resolve$ref} from "../../../utils/utils";
import PlaygroundRunTaskButton from "../../inputs/PlaygroundRunTaskButton.vue"; import PlaygroundRunTaskButton from "../../inputs/PlaygroundRunTaskButton.vue";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import BlockObject from "./tasks/BlockObject.vue";
const {t} = useI18n(); const {t} = useI18n();

View File

@@ -42,6 +42,7 @@
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys"; import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const props = defineProps<{ const props = defineProps<{
root: string,
schema: Schema, schema: Schema,
required?: boolean required?: boolean
}>(); }>();
@@ -162,7 +163,7 @@
}); });
const currentSchemaType = computed(() => const currentSchemaType = computed(() =>
delayedSelectedSchema.value ? getTaskComponent(currentSchema.value) : undefined delayedSelectedSchema.value ? getTaskComponent(currentSchema.value, props.root, definitions.value) : undefined
); );
const isSelectingPlugins = computed(() => schemas.value.length > 4); const isSelectingPlugins = computed(() => schemas.value.length > 4);

View File

@@ -47,7 +47,7 @@
import Add from "../Add.vue"; import Add from "../Add.vue";
import getTaskComponent from "./getTaskComponent"; import getTaskComponent from "./getTaskComponent";
import Wrapper from "./Wrapper.vue"; import Wrapper from "./Wrapper.vue";
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys"; import {BLOCK_SCHEMA_PATH_INJECTION_KEY, SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
defineOptions({inheritAttrs: false}); defineOptions({inheritAttrs: false});
@@ -62,7 +62,7 @@
schema: any; schema: any;
modelValue?: (string | number | boolean | undefined)[] | string | number | boolean; modelValue?: (string | number | boolean | undefined)[] | string | number | boolean;
required?: boolean; required?: boolean;
root?: string; root: string;
}>(), { }>(), {
modelValue: undefined, modelValue: undefined,
schema: () => ({}), schema: () => ({}),
@@ -70,8 +70,10 @@
root: undefined, root: undefined,
}); });
const definitions = inject(SCHEMA_DEFINITIONS_INJECTION_KEY, computed(() => ({})));
const componentType = computed(() => { const componentType = computed(() => {
return getTaskComponent(props.schema.items, props.root); return getTaskComponent(props.schema.items, props.root, definitions.value);
}); });
const needWrapper = computed(() => { const needWrapper = computed(() => {

View File

@@ -2,19 +2,21 @@
<TaskObject <TaskObject
:properties="computedProperties" :properties="computedProperties"
:schema :schema
:root
merge merge
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, inject, ref} from "vue"; import {computed, inject, ref} from "vue";
import TaskObject from "./TaskObject.vue"; import TaskObject from "./BlockObject.vue";
import {resolve$ref} from "../../../../utils/utils"; import {resolve$ref} from "../../../../utils/utils";
import {FULL_SCHEMA_INJECTION_KEY} from "../../injectionKeys"; import {FULL_SCHEMA_INJECTION_KEY} from "../../injectionKeys";
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
schema: any, schema: any,
properties?: Record<string, any>, properties?: Record<string, any>,
root: string
}>(), { }>(), {
properties: undefined, properties: undefined,
}); });

View File

@@ -67,16 +67,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, useTemplateRef, watch} from "vue"; import {computed, ref, useTemplateRef, watch, onMounted, h, inject} from "vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import {DeleteOutline} from "../../utils/icons"; import {DeleteOutline} from "../../utils/icons";
import InputText from "../inputs/InputText.vue"; import InputText from "../inputs/InputText.vue";
import TaskExpression from "./TaskExpression.vue"; import TaskExpression from "./BlockExpression.vue";
import Add from "../Add.vue"; import Add from "../Add.vue";
import getTaskComponent from "./getTaskComponent";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import Wrapper from "./Wrapper.vue"; import Wrapper from "./Wrapper.vue";
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const {t, te} = useI18n(); const {t, te} = useI18n();
@@ -86,20 +87,40 @@
const valueComponent = useTemplateRef<any[]>("valueComponent"); const valueComponent = useTemplateRef<any[]>("valueComponent");
const model = defineModel<Record<string, any>>({
default: () => ({}),
});
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
modelValue?: Record<string, any>;
schema?: any; schema?: any;
root?: string; root: string;
disabled?: boolean; disabled?: boolean;
}>(), { }>(), {
disabled: false, disabled: false,
modelValue: () => ({}),
root: undefined, root: undefined,
schema: () => ({type: "object"}) schema: () => ({type: "object"})
}); });
const definitions = inject(SCHEMA_DEFINITIONS_INJECTION_KEY, computed(() => ({})));
// this convoluted way of importing the getTaskComponent function
// is necessary to avoid circular dependencies
// RollDown might fix it down the road but as of now,
// TaskDict.vue becomes empty in production builds without this lazy loading
const getTaskComponent = ref<(property: any, key: string, definitions: any) => any>(() => {
return h("div", "Loading...");
});
onMounted(async () => {
getTaskComponent.value = (await import("./getTaskComponent")).default;
});
const componentType = computed(() => { const componentType = computed(() => {
return props.schema.additionalProperties ? getTaskComponent(props.schema.additionalProperties, props.root) : null; return props.schema?.additionalProperties ? getTaskComponent.value?.(
props.schema.additionalProperties,
props.root,
definitions.value
) : undefined;
}); });
const currentValue = ref<[string, any][]>([]) const currentValue = ref<[string, any][]>([])
@@ -109,7 +130,7 @@
const localEdit = ref(false); const localEdit = ref(false);
watch( watch(
() => props.modelValue, model,
(newValue) => { (newValue) => {
if(localEdit.value) { if(localEdit.value) {
return; return;
@@ -139,11 +160,9 @@
return; return;
} }
localEdit.value = true; localEdit.value = true;
emit("update:modelValue", Object.fromEntries(currentValue.value.filter(pair => pair[0] !== "" && pair[1] !== undefined))); model.value = Object.fromEntries(currentValue.value.filter(pair => pair[0] !== "" && pair[1] !== undefined));
}, 200); }, 200);
const emit = defineEmits(["update:modelValue"]);
function getKey(key: string) { function getKey(key: string) {
return props.root ? `${props.root}.${key}` : key; return props.root ? `${props.root}.${key}` : key;
} }

View File

@@ -0,0 +1,16 @@
<template>
<NamespaceSelect
data-type="flow"
v-model="model"
:readOnly="!flowStore.isCreating"
allowCreate
/>
</template>
<script setup lang="ts">
import {useFlowStore} from "../../../../stores/flow";
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
const flowStore = useFlowStore();
const model = defineModel<string | undefined>();
</script>

View File

@@ -59,7 +59,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, inject, ref} from "vue"; import {computed, inject, ref} from "vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
import TaskDict from "./TaskDict.vue"; import TaskDict from "./BlockDict.vue";
import Wrapper from "./Wrapper.vue"; import Wrapper from "./Wrapper.vue";
import TaskObjectField from "./TaskObjectField.vue"; import TaskObjectField from "./TaskObjectField.vue";
import {collapseEmptyValues} from "./MixinTask"; import {collapseEmptyValues} from "./MixinTask";
@@ -79,7 +79,7 @@
modelValue?: Model; modelValue?: Model;
required?: boolean; required?: boolean;
schema?: Schema; schema?: Schema;
root?: string; root: string;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -40,7 +40,7 @@
import Task from "./MixinTask"; import Task from "./MixinTask";
import Plus from "vue-material-design-icons/Plus.vue"; import Plus from "vue-material-design-icons/Plus.vue";
import Minus from "vue-material-design-icons/Minus.vue"; import Minus from "vue-material-design-icons/Minus.vue";
import TaskExpression from "./TaskExpression.vue"; import TaskExpression from "./BlockExpression.vue";
import {mapStores} from "pinia"; import {mapStores} from "pinia";
import {useCoreStore} from "../../../../stores/core"; import {useCoreStore} from "../../../../stores/core";
import axios from "axios"; import axios from "axios";

View File

@@ -1,155 +0,0 @@
<template>
<el-form labelPosition="top">
<el-form-item
:key="index"
:required="isRequired(key)"
v-for="(schema, key, index) in properties"
>
<template #label>
<span v-if="required" class="me-1 text-danger">*</span>
<span v-if="getKey(key)" class="label">
{{
getKey(key)
.split(".")
.map(
(word) =>
word.charAt(0).toUpperCase() +
word.slice(1),
)
.join(" ")
}}
</span>
<el-tag disableTransitions size="small" class="ms-2 type-tag">
{{ getTaskComponent(schema, key, properties).ksTaskName }}
</el-tag>
<el-tooltip
v-if="hasTooltip(schema)"
:persistent="false"
:hideAfter="0"
effect="light"
>
<template #content>
<Markdown
class="markdown-tooltip"
:source="helpText(schema)"
/>
</template>
<Help class="ms-2" />
</el-tooltip>
</template>
<component
:is="getTaskComponent(schema, key, properties)"
:modelValue="getPropertiesValue(key)"
@update:model-value="onObjectInput(key, $event)"
:root="getKey(key)"
:schema="schema"
:required="isRequired(key)"
:min="getExclusiveMinimum(key)"
/>
</el-form-item>
</el-form>
</template>
<script setup>
import getTaskComponent from "./getTaskComponent";
import Help from "vue-material-design-icons/HelpBox.vue";
import Markdown from "../../../layout/Markdown.vue";
</script>
<script>
import Task from "./MixinTask";
export default {
name: "TaskBasic",
mixins: [Task],
emits: ["update:modelValue"],
computed: {
properties() {
if (this.schema) {
const properties = this.schema.properties;
return this.sortProperties(properties);
}
return undefined;
},
},
methods: {
getPropertiesValue(properties) {
return this.modelValue && this.modelValue[properties]
? this.modelValue[properties]
: undefined;
},
sortProperties(properties) {
if (!properties) {
return properties;
}
return Object.entries(properties)
.sort((a, b) => {
if (a[0] === "id") {
return -1;
} else if (b[0] === "id") {
return 1;
}
const aRequired = (this.schema.required || []).includes(
a[0],
);
const bRequired = (this.schema.required || []).includes(
b[0],
);
if (aRequired && !bRequired) {
return -1;
} else if (!aRequired && bRequired) {
return 1;
}
const aDefault = "default" in a[1];
const bDefault = "default" in b[1];
if (aDefault && !bDefault) {
return 1;
} else if (!aDefault && bDefault) {
return -1;
}
return a[0].localeCompare(b[0]);
})
.reduce((result, entry) => {
result[entry[0]] = entry[1];
return result;
}, {});
},
onObjectInput(properties, value) {
const currentValue = this.modelValue || {};
currentValue[properties] = value;
this.$emit("update:modelValue", currentValue);
},
hasTooltip(schema) {
return schema.title || schema.description;
},
helpText(schema) {
return (
(schema.title ? "**" + schema.title + "**" : "") +
(schema.title && schema.description ? "\n" : "") +
(schema.description ? schema.description : "")
);
},
getExclusiveMinimum(key) {
const property = this.schema.properties[key];
const propertyHasExclusiveMinimum =
property && property.exclusiveMinimum;
return propertyHasExclusiveMinimum
? property.exclusiveMinimum
: null;
},
},
};
</script>
<style scoped lang="scss">
@import "../../styles/code.scss";
.type-tag {
background-color: var(--ks-tag-background);
color: var(--ks-tag-content);
}
</style>

View File

@@ -1,12 +1,12 @@
<template> <template>
<TaskBoolean <BlockBoolean
v-if="isBoolean" v-if="isBoolean"
v-bind="componentProps" v-bind="componentProps"
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import TaskBoolean from "./TaskBoolean.vue"; import BlockBoolean from "./BlockBoolean.vue";
interface Props { interface Props {
type?: string type?: string

View File

@@ -1,32 +0,0 @@
<template>
<NamespaceSelect
data-type="flow"
:value="modelValue"
:readOnly="!isCreating"
allowCreate
@update:model-value="onInput"
/>
</template>
<script>
import {mapStores} from "pinia";
import Task from "./MixinTask";
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
import {useFlowStore} from "../../../../stores/flow";
export default {
components: {NamespaceSelect},
mixins: [Task],
created() {
const flowNamespace = this.flowStore.flow?.namespace;
if (!this.modelValue && flowNamespace) {
this.onInput(flowNamespace)
}
},
computed: {
...mapStores(useFlowStore),
isCreating() {
return this.flowStore.isCreating;
}
}
};
</script>

View File

@@ -64,12 +64,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, useTemplateRef} from "vue"; import {computed, inject, ref, useTemplateRef} from "vue";
import Help from "vue-material-design-icons/Information.vue"; import Help from "vue-material-design-icons/Information.vue";
import Markdown from "../../../layout/Markdown.vue"; import Markdown from "../../../layout/Markdown.vue";
import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue"; import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue";
import ClearButton from "./ClearButton.vue"; import ClearButton from "./ClearButton.vue";
import getTaskComponent from "./getTaskComponent"; import getTaskComponent from "./getTaskComponent";
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const props = defineProps<{ const props = defineProps<{
schema: any; schema: any;
@@ -134,8 +135,10 @@
return type.value.ksTaskName; return type.value.ksTaskName;
}) })
const definitions = inject(SCHEMA_DEFINITIONS_INJECTION_KEY, computed(() => ({})));
const type = computed(() => { const type = computed(() => {
return getTaskComponent(props.schema, props.fieldKey) return getTaskComponent(props.schema, props.fieldKey, definitions.value)
}) })
</script> </script>

View File

@@ -1,9 +0,0 @@
<template>
<TaskTask @update:model-value="$emit('update:modelValue', $event)" v-bind="$attrs" :section="SECTIONS.TASK_RUNNERS" />
</template>
<script setup>
import {SECTIONS} from "@kestra-io/ui-libs";
import TaskTask from "./TaskTask.vue";
defineEmits(["update:modelValue"]);
</script>

View File

@@ -1,9 +1,7 @@
import {inject} from "vue";
import {pascalCase} from "change-case"; import {pascalCase} from "change-case";
import {resolve$ref} from "../../../../utils/utils"; import {resolve$ref} from "../../../../utils/utils";
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const TasksComponents = import.meta.glob<{ default: any }>("./Task*.vue", {eager: true}); const TasksComponents = import.meta.glob<{ default: any }>("./Block*.vue", {eager: true});
export interface Schema{ export interface Schema{
$ref?: string; $ref?: string;
@@ -18,22 +16,21 @@ export interface Schema{
items?: Schema; items?: Schema;
const?: string; const?: string;
format?: string; format?: string;
enum?: string[];
} }
function getType(property: any, key?: string): string { function getType(property: Schema, key: string | undefined, definitions: Record<string, Schema> | undefined): string {
const definitionsRef = inject(SCHEMA_DEFINITIONS_INJECTION_KEY);
const definitions = definitionsRef?.value;
if (property.enum !== undefined) { if (property.enum !== undefined) {
return "enum"; return "enum";
} }
if (Object.prototype.hasOwnProperty.call(property, "$ref")) { if (Object.prototype.hasOwnProperty.call(property, "$ref") && property.$ref) {
if (property.$ref.includes("tasks.Task")) { if (property.$ref.includes("tasks.Task")) {
return "task" return "task"
} }
if (property.$ref.includes("tasks.runners.TaskRunner")) { if (property.$ref.includes("tasks.runners.TaskRunner")) {
return "task-runner" return "task"
} }
if (property.$ref.includes("io.kestra.preload")) { if (property.$ref.includes("io.kestra.preload")) {
@@ -43,14 +40,14 @@ function getType(property: any, key?: string): string {
return "complex"; return "complex";
} }
if (Object.prototype.hasOwnProperty.call(property, "allOf")) { if (Object.prototype.hasOwnProperty.call(property, "allOf") && property.allOf) {
if (property.allOf.length === 2 if (property.allOf.length === 2
&& property.allOf[0].$ref && !property.allOf[1].properties) { && property.allOf[0].$ref && !property.allOf[1].properties) {
return "complex"; return "complex";
} }
} }
if (Object.prototype.hasOwnProperty.call(property, "anyOf")) { if (Object.prototype.hasOwnProperty.call(property, "anyOf") && property.anyOf) {
if (key === "labels" && property.anyOf.length === 2 if (key === "labels" && property.anyOf.length === 2
&& property.anyOf[0].type === "array" && property.anyOf[1].type === "object") { && property.anyOf[0].type === "array" && property.anyOf[1].type === "object") {
return "dict"; return "dict";
@@ -63,10 +60,6 @@ function getType(property: any, key?: string): string {
return "any-of"; return "any-of";
} }
if (Object.prototype.hasOwnProperty.call(property, "additionalProperties")) {
return "dict";
}
if (property.type === "integer") { if (property.type === "integer") {
return "number"; return "number";
} }
@@ -89,7 +82,7 @@ function getType(property: any, key?: string): string {
return "subflow-inputs"; return "subflow-inputs";
} }
if (property.type === "array") { if (property.type === "array" && property.items) {
const items = definitions ? resolve$ref({definitions: definitions}, property.items) : property.items; const items = definitions ? resolve$ref({definitions: definitions}, property.items) : property.items;
if (items?.anyOf?.length === 0 || items?.anyOf?.length > 10 || key === "pluginDefaults" || key === "layout") { if (items?.anyOf?.length === 0 || items?.anyOf?.length > 10 || key === "pluginDefaults" || key === "layout") {
return "list"; return "list";
@@ -98,6 +91,10 @@ function getType(property: any, key?: string): string {
return "array"; return "array";
} }
if (Object.prototype.hasOwnProperty.call(property, "additionalProperties")) {
return "dict";
}
if (property.const) { if (property.const) {
return "constant" return "constant"
} }
@@ -106,13 +103,13 @@ function getType(property: any, key?: string): string {
return "dict"; return "dict";
} }
return property.type || "expression"; return typeof property.type === "string" ? property.type : "expression";
} }
export default function getTaskComponent(property: any, key?: string): any { export default function getTaskComponent(property: any, key: string, definitions: Record<string, Schema>): any {
const typeString = getType(property, key); const typeString = getType(property, key, definitions);
const type = pascalCase(typeString); const type = pascalCase(typeString);
const component = TasksComponents[`./Task${type}.vue`]?.default; const component = TasksComponents[`./Block${type}.vue`]?.default;
if (component) { if (component) {
component.ksTaskName = typeString; component.ksTaskName = typeString;
} }

View File

@@ -26,7 +26,7 @@
tooltip, tooltip,
getFormat, getFormat,
} from "../dashboard/composables/charts"; } from "../dashboard/composables/charts";
import Logs from "../../utils/logs"; import * as Logs from "../../utils/logs";
export default defineComponent({ export default defineComponent({
components: {Bar}, components: {Bar},

View File

@@ -12,7 +12,7 @@
import Auth from "../../override/components/auth/Auth.vue"; import Auth from "../../override/components/auth/Auth.vue";
withDefaults(defineProps<{ withDefaults(defineProps<{
showLink: boolean showLink?: boolean
}>(), { }>(), {
showLink: true showLink: true
}); });

View File

@@ -54,7 +54,6 @@ interface FlowValidations {
export interface Flow { export interface Flow {
id: string; id: string;
namespace: string; namespace: string;
disabled?: boolean;
source: string; source: string;
revision?: number; revision?: number;
deleted?: boolean; deleted?: boolean;

View File

@@ -220,13 +220,13 @@ export const usePluginsStore = defineStore("plugins", () => {
const apiStore = useApiStore(); const apiStore = useApiStore();
const apiPromise = apiStore.pluginIcons().then(response => { const apiPromise = apiStore.pluginIcons().then(async response => {
apiIcons.value = response.data ?? {}; apiIcons.value = response.data ?? {};
return response.data; return response.data;
}); });
const iconsPromise = const iconsPromise =
axios.get(`${apiUrlWithoutTenants()}/plugins/icons`, {}).then(response => { axios.get(`${apiUrlWithoutTenants()}/plugins/icons`, {}).then(async response => {
pluginsIcons.value = response.data ?? {}; pluginsIcons.value = response.data ?? {};
return pluginsIcons.value; return pluginsIcons.value;
}); });
@@ -241,7 +241,7 @@ export const usePluginsStore = defineStore("plugins", () => {
function groupIcons() { function groupIcons() {
return axios.get(`${apiUrlWithoutTenants()}/plugins/icons/groups`, {}) return axios.get(`${apiUrlWithoutTenants()}/plugins/icons/groups`, {})
.then(response => { .then(async response => {
return response.data; return response.data;
}); });
} }

View File

@@ -1,35 +0,0 @@
export default class FlowUtils {
static findTaskById(flow, taskId) {
let result = this.loopOver(flow, (value) => {
if (value instanceof Object) {
if (value.type !== undefined && value.id === taskId) {
return true;
}
}
return false;
});
return result.length > 0 ? result[0] : undefined;
}
static loopOver(item, predicate, result) {
if (result === undefined) {
result = [];
}
if (predicate(item)) {
result.push(item);
}
if (Array.isArray(item)) {
item.flatMap(item => this.loopOver(item, predicate, result));
} else if (item instanceof Object) {
Object.entries(item).flatMap(([_key, value]) => {
this.loopOver(value, predicate, result);
});
}
return result;
}
}

33
ui/src/utils/flowUtils.ts Normal file
View File

@@ -0,0 +1,33 @@
export function findTaskById(flow: any, taskId: string) {
const result = loopOver(flow, (value: any) => {
if (value instanceof Object) {
if (value.type !== undefined && value.id === taskId) {
return true;
}
}
return false;
});
return result.length > 0 ? result[0] : undefined;
}
export function loopOver(item: any, predicate: (item: any) => boolean, result: any[] | undefined = undefined) {
if (result === undefined) {
result = [];
}
if (predicate(item)) {
result.push(item);
}
if (Array.isArray(item)) {
item.flatMap(item => loopOver(item, predicate, result));
} else if (item instanceof Object) {
Object.entries(item).flatMap(([_key, value]) => {
loopOver(value, predicate, result);
});
}
return result;
}

View File

@@ -1,68 +0,0 @@
import {cssVariable} from "@kestra-io/ui-libs";
const LEVELS = [
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
];
export default class Logs {
static color() {
return Object.fromEntries(LEVELS.map(level => [level, cssVariable("--log-chart-" + level.toLowerCase())]));
}
static graphColors(state) {
const COLORS = {
ERROR: "#AB0009",
WARN: "#DD5F00",
INFO: "#029E73",
DEBUG: "#1761FD",
TRACE: "#8405FF",
};
return COLORS[state];
}
static chartColorFromLevel(level, alpha = 1) {
const hex = Logs.color()[level];
if (!hex) {
return null;
}
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${alpha})`;
}
static sort(value) {
return Object.keys(value)
.sort((a, b) => {
return Logs.index(LEVELS, a) - Logs.index(LEVELS, b);
})
.reduce(
(obj, key) => {
obj[key] = value[key];
return obj;
},
{}
);
}
static index(based, value) {
const index = based.indexOf(value);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
}
static levelOrLower(level) {
const levels = [];
for (const currentLevel of LEVELS) {
levels.push(currentLevel);
if (currentLevel === level) {
break;
}
}
return levels.reverse();
}
}

66
ui/src/utils/logs.ts Normal file
View File

@@ -0,0 +1,66 @@
import {cssVariable} from "@kestra-io/ui-libs";
const LEVELS = [
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
];
export function color() {
return Object.fromEntries(LEVELS.map(level => [level, cssVariable("--log-chart-" + level.toLowerCase())])) as Record<typeof LEVELS[number], string>;
}
const COLORS: Record<typeof LEVELS[number], string> = {
ERROR: "#AB0009",
WARN: "#DD5F00",
INFO: "#029E73",
DEBUG: "#1761FD",
TRACE: "#8405FF",
};
export function graphColors(state: typeof LEVELS[number]) {
return COLORS[state];
}
export function chartColorFromLevel(level: typeof LEVELS[number], alpha = 1) {
const hex = color()[level];
if (!hex) {
return null;
}
const [r, g, b] = hex.match(/\w\w/g)?.map(x => parseInt(x, 16)) ?? [];
return `rgba(${r},${g},${b},${alpha})`;
}
export function sort(value: Record<string, any>) {
return Object.keys(value)
.sort((a, b) => {
return index(LEVELS, a) - index(LEVELS, b);
})
.reduce(
(obj, key) => {
obj[key] = value[key];
return obj;
},
{} as Record<string, any>
);
}
export function index(based: string[], value: string) {
const index = based.indexOf(value);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
}
export function levelOrLower(level: typeof LEVELS[number]) {
const levels = [];
for (const currentLevel of LEVELS) {
levels.push(currentLevel);
if (currentLevel === level) {
break;
}
}
return levels.reverse();
}

View File

@@ -1,5 +1,5 @@
import {computed, provide, ref} from "vue"; import {computed, provide, ref} from "vue";
import TaskDict from "../../../../../../src/components/no-code/components/tasks/TaskDict.vue"; import TaskDict from "../../../../../../src/components/no-code/components/tasks/BlockDict.vue";
import Wrapper from "../../../../../../src/components/no-code/components/tasks/Wrapper.vue"; import Wrapper from "../../../../../../src/components/no-code/components/tasks/Wrapper.vue";
import {userEvent, waitFor, within, expect} from "storybook/internal/test"; import {userEvent, waitFor, within, expect} from "storybook/internal/test";
import {Meta, StoryObj} from "@storybook/vue3-vite"; import {Meta, StoryObj} from "@storybook/vue3-vite";

View File

@@ -1,4 +1,4 @@
import TaskObject from "../../../../../../src/components/no-code/components/tasks/TaskObject.vue"; import TaskObject from "../../../../../../src/components/no-code/components/tasks/BlockObject.vue";
import {computed, provide, ref} from "vue" import {computed, provide, ref} from "vue"
import {StoryObj} from "@storybook/vue3-vite"; import {StoryObj} from "@storybook/vue3-vite";
import {waitFor, within, expect, fireEvent} from "storybook/test"; import {waitFor, within, expect, fireEvent} from "storybook/test";

View File

@@ -1,6 +1,6 @@
import {describe, it, expect} from "vitest" import {describe, it, expect} from "vitest"
import {YamlUtils as YAML_UTILS} from "@kestra-io/ui-libs"; import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import FlowUtils from "../../../src/utils/flowUtils"; import * as FlowUtils from "../../../src/utils/flowUtils";
export const flat = ` export const flat = `
id: flat id: flat
@@ -64,32 +64,32 @@ tasks:
describe("FlowUtils", () => { describe("FlowUtils", () => {
it("extractTask from a flat flow", () => { it("extractTask from a flat flow", () => {
let flow = YAML_UTILS.parse(flat); const flow = YAML_UTILS.parse(flat);
let findTaskById = FlowUtils.findTaskById(flow, "1-2"); const findTaskById = FlowUtils.findTaskById(flow, "1-2");
expect(findTaskById.id).toBe("1-2"); expect(findTaskById.id).toBe("1-2");
expect(findTaskById.type).toBe("io.kestra.plugin.core.log.Log"); expect(findTaskById.type).toBe("io.kestra.plugin.core.log.Log");
}) })
it("extractTask from a flowable flow", () => { it("extractTask from a flowable flow", () => {
let flow = YAML_UTILS.parse(flowable); const flow = YAML_UTILS.parse(flowable);
let findTaskById = FlowUtils.findTaskById(flow, "1-2"); const findTaskById = FlowUtils.findTaskById(flow, "1-2");
expect(findTaskById.id).toBe("1-2"); expect(findTaskById.id).toBe("1-2");
expect(findTaskById.type).toBe("io.kestra.plugin.core.log.Log"); expect(findTaskById.type).toBe("io.kestra.plugin.core.log.Log");
}) })
it("extractTask from a flowable flow", () => { it("extractTask from a flowable flow", () => {
let flow = YAML_UTILS.parse(plugins); const flow = YAML_UTILS.parse(plugins);
let findTaskById = FlowUtils.findTaskById(flow, "nest-1"); const findTaskById = FlowUtils.findTaskById(flow, "nest-1");
expect(findTaskById.id).toBe("nest-1"); expect(findTaskById.id).toBe("nest-1");
expect(findTaskById.type).toBe("io.kestra.core.tasks.unittest.Example"); expect(findTaskById.type).toBe("io.kestra.core.tasks.unittest.Example");
}) })
it("missing task from a flowable flow", () => { it("missing task from a flowable flow", () => {
let flow = YAML_UTILS.parse(flowable); const flow = YAML_UTILS.parse(flowable);
let findTaskById = FlowUtils.findTaskById(flow, "undefined"); const findTaskById = FlowUtils.findTaskById(flow, "undefined");
expect(findTaskById).toBeUndefined(); expect(findTaskById).toBeUndefined();
}) })