mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
6 Commits
dependabot
...
fix/load-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a1a6c104 | ||
|
|
35bce0b5b8 | ||
|
|
e2b46c4cce | ||
|
|
49264cb7f9 | ||
|
|
82e139de03 | ||
|
|
7d64692f0f |
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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(() => {
|
||||||
@@ -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,
|
||||||
});
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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<{
|
||||||
@@ -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";
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
33
ui/src/utils/flowUtils.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
66
ui/src/utils/logs.ts
Normal 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();
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user