Compare commits

...

6 Commits

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

View File

@@ -97,7 +97,7 @@
import {State} from "@kestra-io/ui-libs"
import Duration from "../layout/Duration.vue";
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 {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller";
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";

View File

@@ -130,7 +130,7 @@
import Utils from "../../utils/utils";
import LogLine from "../logs/LogLine.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 {mapStores} from "pinia";
import {useExecutionsStore} from "../../stores/executions";

View File

@@ -32,7 +32,7 @@
import permission from "../../models/permission";
import action from "../../models/action";
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 InputsForm from "../../components/inputs/InputsForm.vue";
import {inputsToFormData} from "../../utils/submitTask";

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -42,6 +42,7 @@
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const props = defineProps<{
root: string,
schema: Schema,
required?: boolean
}>();
@@ -162,7 +163,7 @@
});
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);

View File

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

View File

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

View File

@@ -67,16 +67,17 @@
</template>
<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 {DeleteOutline} from "../../utils/icons";
import InputText from "../inputs/InputText.vue";
import TaskExpression from "./TaskExpression.vue";
import TaskExpression from "./BlockExpression.vue";
import Add from "../Add.vue";
import getTaskComponent from "./getTaskComponent";
import debounce from "lodash/debounce";
import Wrapper from "./Wrapper.vue";
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const {t, te} = useI18n();
@@ -86,20 +87,40 @@
const valueComponent = useTemplateRef<any[]>("valueComponent");
const model = defineModel<Record<string, any>>({
default: () => ({}),
});
const props = withDefaults(defineProps<{
modelValue?: Record<string, any>;
schema?: any;
root?: string;
root: string;
disabled?: boolean;
}>(), {
disabled: false,
modelValue: () => ({}),
root: undefined,
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(() => {
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][]>([])
@@ -109,7 +130,7 @@
const localEdit = ref(false);
watch(
() => props.modelValue,
model,
(newValue) => {
if(localEdit.value) {
return;
@@ -139,11 +160,9 @@
return;
}
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);
const emit = defineEmits(["update:modelValue"]);
function getKey(key: string) {
return props.root ? `${props.root}.${key}` : key;
}

View File

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

View File

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

View File

@@ -40,7 +40,7 @@
import Task from "./MixinTask";
import Plus from "vue-material-design-icons/Plus.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 {useCoreStore} from "../../../../stores/core";
import axios from "axios";

View File

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

View File

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

View File

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

View File

@@ -64,12 +64,13 @@
</template>
<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 Markdown from "../../../layout/Markdown.vue";
import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue";
import ClearButton from "./ClearButton.vue";
import getTaskComponent from "./getTaskComponent";
import {SCHEMA_DEFINITIONS_INJECTION_KEY} from "../../injectionKeys";
const props = defineProps<{
schema: any;
@@ -134,8 +135,10 @@
return type.value.ksTaskName;
})
const definitions = inject(SCHEMA_DEFINITIONS_INJECTION_KEY, computed(() => ({})));
const type = computed(() => {
return getTaskComponent(props.schema, props.fieldKey)
return getTaskComponent(props.schema, props.fieldKey, definitions.value)
})
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

@@ -1,5 +1,5 @@
import {computed, provide, ref} from "vue";
import 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 {userEvent, waitFor, within, expect} from "storybook/internal/test";
import {Meta, StoryObj} from "@storybook/vue3-vite";

View File

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

View File

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