Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ui/packages/design-system/src/components/Form/KsEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,11 @@
color: var(--ks-text-inactive) !important;
}

.monaco-editor .codelens-decoration > a:hover,
.monaco-editor .codelens-decoration > a:hover .codicon {
color: var(--ks-text-link) !important;
}

.ks-monaco-editor {
position: absolute;
width: 100%;
Expand Down
42 changes: 42 additions & 0 deletions ui/packages/topology/src/utils/flowYamlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,48 @@ export function extractFieldFromMaps<T extends string>(
return maps
}

export interface TypedBlock {
type: string;
value: Record<string, any>;
range: Range;
path: string;
}

export function extractTypedBlocks(source: string): TypedBlock[] {
return extractTypedBlocksWithMeta(source).blocks
}

export interface FlowSourceData {
blocks: TypedBlock[];
namespace?: string;
id?: string;
}

export function extractTypedBlocksWithMeta(source: string): FlowSourceData {
const yamlDoc = parseDocument(source) as any
const blocks: TypedBlock[] = []
visit(yamlDoc, {
Map(_, map: any, parents) {
const type = map.items?.find((item: any) => item?.key?.value === "type")?.value?.value
if (typeof type === "string" && map.range) {
const path = parents
.filter((parent) => isPair(parent))
.map((parent: any) => parent?.key?.value)
.join(".")
blocks.push({type, value: map.toJSON(), range: map.range, path})
}
},
})
const root = yamlDoc.contents
const namespace = isMap(root) ? root.get("namespace") : undefined
const id = isMap(root) ? root.get("id") : undefined
return {
blocks,
namespace: typeof namespace === "string" ? namespace : undefined,
id: typeof id === "string" ? id : undefined,
}
}

function extractAllTypes(source: string, validTypes: string[] = []){
return extractFieldFromMaps(source, "type", () => true, (value) =>
validTypes.some((t) => t === value),
Expand Down
90 changes: 89 additions & 1 deletion ui/packages/topology/tests/units/utils/flowYamlUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1447,4 +1447,92 @@ describe("stringify with preserveCronQuotes", () => {
const result = YamlUtils.stringify(yaml)
expect(result).toContain("cron: \"0 0 * * *\"")
})
})
})
describe("extractTypedBlocks", () => {
test("returns every block carrying a type with its range", () => {
const source = `id: my-flow
namespace: company.team
tasks:
- id: hello
type: io.kestra.plugin.core.log.Log
message: Hello World
triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: admin1234
`

const blocks = YamlUtils.extractTypedBlocks(source)

const types = blocks.map((block) => block.type)
expect(types).toContain("io.kestra.plugin.core.log.Log")
expect(types).toContain("io.kestra.plugin.core.trigger.Webhook")

const webhook = blocks.find((block) => block.type === "io.kestra.plugin.core.trigger.Webhook")
expect(webhook?.value.id).toBe("webhook")
expect(webhook?.value.key).toBe("admin1234")
expect(webhook?.range[0]).toBeTypeOf("number")
expect(webhook?.path).toBe("triggers")

const log = blocks.find((block) => block.type === "io.kestra.plugin.core.log.Log")
expect(log?.path).toBe("tasks")
})

test("captures the parent section path so artifacts can scope to it", () => {
const source = `id: my-flow
namespace: company.team
pluginDefaults:
- type: io.kestra.plugin.core.trigger.Webhook
values:
key: shared
triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: admin1234
`

const webhookBlocks = YamlUtils.extractTypedBlocks(source)
.filter((block) => block.type === "io.kestra.plugin.core.trigger.Webhook")

expect(webhookBlocks.map((block) => block.path).sort()).toEqual(["pluginDefaults", "triggers"])
})

test("ignores blocks without a type", () => {
const source = `id: my-flow
namespace: company.team
`
expect(YamlUtils.extractTypedBlocks(source)).toEqual([])
})
})

describe("extractTypedBlocksWithMeta", () => {
test("returns blocks alongside top-level namespace and id in a single parse", () => {
const source = `id: my-flow
namespace: company.team
triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: admin1234
`

const {blocks, namespace, id} = YamlUtils.extractTypedBlocksWithMeta(source)

expect(namespace).toBe("company.team")
expect(id).toBe("my-flow")
expect(blocks).toHaveLength(1)
expect(blocks[0].type).toBe("io.kestra.plugin.core.trigger.Webhook")
})

test("returns undefined namespace and id when not present in the source", () => {
const source = `triggers:
- id: webhook
type: io.kestra.plugin.core.trigger.Webhook
key: admin1234
`

const {namespace, id} = YamlUtils.extractTypedBlocksWithMeta(source)

expect(namespace).toBeUndefined()
expect(id).toBeUndefined()
})
})
3 changes: 2 additions & 1 deletion ui/src/components/flows/FlowRun.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
import type {Flow} from "../../stores/flow"
import {executeTask} from "../../utils/submitTask"
import {executeFlowBehaviours, storageKeys} from "../../utils/constants"
import {WEBHOOK_TRIGGER_TYPE} from "../../utils/webhook"
import {normalize} from "../../utils/inputs"
import type {InputType} from "../../utils/inputs"
import type {FormInstance} from "@kestra-io/design-system"
Expand Down Expand Up @@ -212,7 +213,7 @@
return false
}
return flow.value.triggers.some(trigger =>
trigger.type === "io.kestra.plugin.core.trigger.Webhook" &&
trigger.type === WEBHOOK_TRIGGER_TYPE &&
("disabled" in trigger ? trigger.disabled === undefined || trigger.disabled === false : true),
)
})
Expand Down
10 changes: 3 additions & 7 deletions ui/src/components/flows/TriggerAvatar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
</template>
<script setup lang="ts">
import {computed, ref, nextTick} from "vue"
import {useRoute} from "vue-router"
import {usePluginsStore} from "../../stores/plugins"
import * as Utils from "../../utils/utils"
import {webhookUrl, WEBHOOK_TRIGGER_TYPE} from "../../utils/webhook"
import TriggerVars from "./TriggerVars.vue"
import {KsTaskIcon} from "@kestra-io/design-system"
import {useI18n} from "vue-i18n"
Expand Down Expand Up @@ -55,7 +55,6 @@
}>()

const pluginsStore = usePluginsStore()
const route = useRoute()

const popoverRefs = ref<Map<string, any>>(new Map())

Expand Down Expand Up @@ -95,11 +94,8 @@
const toast = useToast()

async function copyLink(trigger: Trigger) {
if (trigger?.type === "io.kestra.plugin.core.trigger.Webhook" && props.flow) {
const tenant = route.params.tenant ? route.params.tenant + "/" : ""
const url =
new URL(window.location.href).origin +
`/api/v1/${tenant}executions/webhook/${props.flow.namespace}/${props.flow.id}/${trigger.key}`
if (trigger?.type === WEBHOOK_TRIGGER_TYPE && trigger.key && props.flow) {
const url = webhookUrl({namespace: props.flow.namespace, id: props.flow.id, key: trigger.key})
try {
await Utils.copy(url)
toast.success(t("webhook link copied"))
Expand Down
12 changes: 4 additions & 8 deletions ui/src/components/flows/WebhookCurl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import CopyToClipboard from "../layout/CopyToClipboard.vue"
import {KsEditor} from "@kestra-io/design-system"
import {useEditorBindings} from "../../composables/useEditorBindings"
import {baseUrl, basePath, apiUrl} from "override/utils/route"
import {webhookUrl, WEBHOOK_TRIGGER_TYPE} from "../../utils/webhook"
import {useFlowStore} from "../../stores/flow"

interface Flow {
Expand Down Expand Up @@ -68,22 +68,18 @@
}

return sourceFlow.triggers.filter((trigger: Trigger) =>
trigger.type === "io.kestra.plugin.core.trigger.Webhook" &&
trigger.type === WEBHOOK_TRIGGER_TYPE &&
(trigger.disabled === undefined || trigger.disabled === false),
)
})

const generateWebhookUrl = (trigger: Trigger): string => {
const origin = baseUrl ? apiUrl() : `${location.origin}${basePath()}`
return `${origin}/executions/webhook/${props.flow.namespace}/${props.flow.id}/${trigger.key}`
}

const generateWebhookCurlCommand = (trigger: Trigger): string => {
if (!trigger.key) {
return "Webhook key not available"
}

const command = [`curl -X POST ${generateWebhookUrl(trigger)}`]
const url = webhookUrl({namespace: props.flow.namespace, id: props.flow.id, key: trigger.key})
const command = [`curl -X POST ${url}`]
command.push("-H \"Content-Type: application/json\"")

if (webhookPayload.value.trim()) {
Expand Down
55 changes: 55 additions & 0 deletions ui/src/composables/monaco/artifacts/editorArtifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export const ARTIFACT_COPY_COMMAND = "kestra.editorArtifact.copyToClipboard"

export interface ArtifactBlock {
type: string;
value: Record<string, any>;
range: [number, number, number];
path: string;
}

export interface EditorArtifactContext {
namespace?: string;
id?: string;
t: (key: string, named?: Record<string, unknown>) => string;
}

export interface EditorArtifactLens {
title: string;
command: {id: string; arguments?: unknown[]};
}

export interface EditorArtifactProvider {
type: string;
provide(block: ArtifactBlock, context: EditorArtifactContext): EditorArtifactLens[];
}

export interface ResolvedArtifact {
range: [number, number, number];
lens: EditorArtifactLens;
}

const providers: EditorArtifactProvider[] = []

export function registerEditorArtifactProvider(provider: EditorArtifactProvider): void {
if (!providers.some((existing) => existing.type === provider.type)) {
providers.push(provider)
}
}

export function provideEditorArtifacts(
blocks: ArtifactBlock[],
context: EditorArtifactContext,
): ResolvedArtifact[] {
const resolved: ResolvedArtifact[] = []
for (const block of blocks) {
for (const provider of providers) {
if (provider.type !== block.type) {
continue
}
for (const lens of provider.provide(block, context)) {
resolved.push({range: block.range, lens})
}
}
}
return resolved
}
6 changes: 6 additions & 0 deletions ui/src/composables/monaco/artifacts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {registerEditorArtifactProvider} from "./editorArtifacts"
import {webhookArtifactProvider} from "./webhookArtifact"

registerEditorArtifactProvider(webhookArtifactProvider)

export * from "./editorArtifacts"
24 changes: 24 additions & 0 deletions ui/src/composables/monaco/artifacts/webhookArtifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {webhookUrl, WEBHOOK_TRIGGER_TYPE} from "../../../utils/webhook"
import {ARTIFACT_COPY_COMMAND, type EditorArtifactProvider} from "./editorArtifacts"

export const webhookArtifactProvider: EditorArtifactProvider = {
type: WEBHOOK_TRIGGER_TYPE,
provide(block, context) {
const key = block.value?.key
if (block.path !== "triggers" || !key || !context.namespace || !context.id) {
return []
}

const url = webhookUrl({namespace: context.namespace, id: context.id, key})

return [
{
title: context.t("webhook.copy_url"),
command: {
id: ARTIFACT_COPY_COMMAND,
arguments: [{text: url, message: context.t("webhook link copied")}],
},
},
]
},
}
Loading
Loading