mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-25 11:12:12 -05:00
Compare commits
1 Commits
docs/purge
...
test/merge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2dfd0d9bc |
@@ -41,7 +41,9 @@
|
||||
<template #expand>
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
<LogsWrapper class="m-3" :filters="props.row" v-if="hasLogsContent(props.row)" :withCharts="false" embed />
|
||||
<LogsWrapper class="m-3" :filters="props.row" v-if="hasLogsContent(props.row)"
|
||||
:withCharts="false" embed
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
@@ -71,6 +73,9 @@
|
||||
<el-button @click="deleteBackfills()">
|
||||
{{ $t("delete backfills") }}
|
||||
</el-button>
|
||||
<el-button @click="deleteTriggers()" type="danger">
|
||||
{{ $t("delete triggers") }}
|
||||
</el-button>
|
||||
</BulkSelect>
|
||||
</template>
|
||||
<el-table-column
|
||||
@@ -95,17 +100,23 @@
|
||||
:sortOrders="['flowId', 'namespace', 'nextExecutionDate'].includes(col.prop) ? ['ascending', 'descending'] : undefined"
|
||||
>
|
||||
<template #header v-if="col.prop === 'date'">
|
||||
<el-tooltip :content="$t('last trigger date tooltip')" placement="top" effect="light" popperClass="wide-tooltip">
|
||||
<el-tooltip :content="$t('last trigger date tooltip')" placement="top" effect="light"
|
||||
popperClass="wide-tooltip"
|
||||
>
|
||||
<span>{{ col.label }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #header v-else-if="col.prop === 'updatedDate'">
|
||||
<el-tooltip :content="$t('context updated date tooltip')" placement="top" effect="light" popperClass="wide-tooltip">
|
||||
<el-tooltip :content="$t('context updated date tooltip')" placement="top" effect="light"
|
||||
popperClass="wide-tooltip"
|
||||
>
|
||||
<span>{{ col.label }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #header v-else-if="col.prop === 'nextExecutionDate'">
|
||||
<el-tooltip :content="$t('next evaluation date tooltip')" placement="top" effect="light" popperClass="wide-tooltip">
|
||||
<el-tooltip :content="$t('next evaluation date tooltip')" placement="top" effect="light"
|
||||
popperClass="wide-tooltip"
|
||||
>
|
||||
<span>{{ col.label }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -181,13 +192,24 @@
|
||||
<LockOff />
|
||||
</Kicon>
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Kicon
|
||||
:tooltip="$t('delete trigger')"
|
||||
placement="left"
|
||||
@click="confirmDeleteTrigger(scope.row)"
|
||||
>
|
||||
<Delete />
|
||||
</Kicon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('backfill')" columnKey="backfill">
|
||||
<template #default="scope">
|
||||
<div class="backfillContainer items-center gap-2">
|
||||
<span v-if="scope.row.backfill" class="statusIcon">
|
||||
<el-tooltip v-if="!scope.row.backfill.paused" :content="$t('backfill running')" effect="light">
|
||||
<el-tooltip v-if="!scope.row.backfill.paused" :content="$t('backfill running')"
|
||||
effect="light"
|
||||
>
|
||||
<PlayBox font />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else :content="$t('backfill paused')">
|
||||
@@ -297,7 +319,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import _merge from "lodash/merge";
|
||||
import {ref, computed, watch} from "vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute} from "vue-router";
|
||||
import {ElMessage} from "element-plus";
|
||||
@@ -311,18 +333,16 @@
|
||||
import {useTriggerFilter} from "../filter/configurations";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
import {useTableColumns, type ColumnConfig} from "../../composables/useTableColumns";
|
||||
import {type ColumnConfig, useTableColumns} from "../../composables/useTableColumns";
|
||||
|
||||
import action from "../../models/action";
|
||||
import permission from "../../models/permission";
|
||||
|
||||
const triggerFilter = useTriggerFilter();
|
||||
|
||||
import LockOff from "vue-material-design-icons/LockOff.vue";
|
||||
import PlayBox from "vue-material-design-icons/PlayBox.vue";
|
||||
import PauseBox from "vue-material-design-icons/PauseBox.vue";
|
||||
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
|
||||
import CalendarCollapseHorizontalOutline from "vue-material-design-icons/CalendarCollapseHorizontalOutline.vue";
|
||||
import Delete from "vue-material-design-icons/Delete.vue";
|
||||
|
||||
import Id from "../Id.vue";
|
||||
import Kicon from "../Kicon.vue";
|
||||
@@ -341,6 +361,8 @@
|
||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
|
||||
const triggerFilter = useTriggerFilter();
|
||||
|
||||
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
@@ -369,55 +391,55 @@
|
||||
end: null,
|
||||
inputs: null,
|
||||
labels: []
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const optionalColumns = computed(() => [
|
||||
{
|
||||
label: t("flow"),
|
||||
prop: "flowId",
|
||||
default: true,
|
||||
label: t("flow"),
|
||||
prop: "flowId",
|
||||
default: true,
|
||||
description: t("filter.table_column.triggers.flow")
|
||||
},
|
||||
{
|
||||
label: t("namespace"),
|
||||
prop: "namespace",
|
||||
default: true,
|
||||
label: t("namespace"),
|
||||
prop: "namespace",
|
||||
default: true,
|
||||
description: t("filter.table_column.triggers.namespace")
|
||||
},
|
||||
{
|
||||
label: t("current execution"),
|
||||
prop: "executionId",
|
||||
default: false,
|
||||
label: t("current execution"),
|
||||
prop: "executionId",
|
||||
default: false,
|
||||
description: t("filter.table_column.triggers.current execution")
|
||||
},
|
||||
{
|
||||
label: t("workerId"),
|
||||
prop: "workerId",
|
||||
default: false,
|
||||
label: t("workerId"),
|
||||
prop: "workerId",
|
||||
default: false,
|
||||
description: t("filter.table_column.triggers.workerId")
|
||||
},
|
||||
{
|
||||
label: t("last trigger date"),
|
||||
prop: "date",
|
||||
default: true,
|
||||
label: t("last trigger date"),
|
||||
prop: "date",
|
||||
default: true,
|
||||
description: t("filter.table_column.triggers.last trigger date")
|
||||
},
|
||||
{
|
||||
label: t("context updated date"),
|
||||
prop: "updatedDate",
|
||||
default: false,
|
||||
label: t("context updated date"),
|
||||
prop: "updatedDate",
|
||||
default: false,
|
||||
description: t("filter.table_column.triggers.context updated date")
|
||||
},
|
||||
{
|
||||
label: t("next evaluation date"),
|
||||
prop: "nextExecutionDate",
|
||||
default: false,
|
||||
label: t("next evaluation date"),
|
||||
prop: "nextExecutionDate",
|
||||
default: false,
|
||||
description: t("filter.table_column.triggers.next evaluation date")
|
||||
},
|
||||
{
|
||||
label: t("evaluation lock date"),
|
||||
prop: "evaluateRunningDate",
|
||||
default: false,
|
||||
label: t("evaluation lock date"),
|
||||
prop: "evaluateRunningDate",
|
||||
default: false,
|
||||
description: t("filter.table_column.triggers.evaluation lock date")
|
||||
}
|
||||
]);
|
||||
@@ -430,7 +452,7 @@
|
||||
initialVisibleColumns: optionalColumns.value.filter(col => col.default).map(col => col.prop)
|
||||
});
|
||||
|
||||
const visibleColumns = computed(() =>
|
||||
const visibleColumns = computed(() =>
|
||||
displayColumns.value
|
||||
.map(prop => optionalColumns.value.find(c => c.prop === prop))
|
||||
.filter(Boolean) as ColumnConfig[]
|
||||
@@ -468,7 +490,7 @@
|
||||
});
|
||||
|
||||
const {
|
||||
queryBulkAction,
|
||||
queryBulkAction,
|
||||
selection,
|
||||
handleSelectionChange,
|
||||
toggleAllUnselected,
|
||||
@@ -544,7 +566,7 @@
|
||||
const disabledEndDate = (time: Date) => {
|
||||
return new Date() < time || (backfill.value.start && backfill.value.start > time);
|
||||
};
|
||||
|
||||
|
||||
const triggerLoadDataAfterBulkEditAction = () => {
|
||||
loadData();
|
||||
setTimeout(() => loadData(), 200);
|
||||
@@ -601,9 +623,44 @@
|
||||
});
|
||||
};
|
||||
|
||||
const genericConfirmAction = (toastKey: string, queryAction: string, byIdAction: string, success: string, data?: any) => {
|
||||
const confirmDeleteTrigger = (trigger) => {
|
||||
toast.confirm(
|
||||
t(toastKey, {"count": queryBulkAction.value ? total.value : selection.value?.length}) + ". " + t("bulk action async warning"),
|
||||
t("delete trigger confirmation", {id: trigger.id}),
|
||||
() => triggerStore.delete({
|
||||
namespace: trigger.namespace,
|
||||
flowId: trigger.flowId,
|
||||
triggerId: trigger.triggerId
|
||||
}).then(() => {
|
||||
toast.success(t("delete trigger success", {id: trigger.id}));
|
||||
loadData();
|
||||
}).catch(error => {
|
||||
toast.error(t("delete trigger error", {id: trigger.id}));
|
||||
console.error(error);
|
||||
}),
|
||||
"warning"
|
||||
);
|
||||
};
|
||||
|
||||
const deleteTriggers = () => {
|
||||
genericConfirmAction(
|
||||
"bulk delete triggers",
|
||||
"deleteByQuery",
|
||||
"deleteByTriggers",
|
||||
"bulk success delete triggers",
|
||||
null,
|
||||
"WARNING: deleting triggers may lead to duplicate executions if the triggers are still active in flows"
|
||||
);
|
||||
};
|
||||
|
||||
const genericConfirmAction = (toastKey: string, queryAction: string, byIdAction: string, success: string, data?: any, extraWarning = null) => {
|
||||
let message = t(toastKey, {"count": queryBulkAction.value ? total.value : selection.value?.length}) + ". " + t("bulk action async warning");
|
||||
|
||||
if (extraWarning) {
|
||||
message += "<br><br><strong>" + extraWarning + "</strong>";
|
||||
}
|
||||
|
||||
toast.confirm(
|
||||
message,
|
||||
() => genericConfirmCallback(queryAction, byIdAction, success, data)
|
||||
);
|
||||
};
|
||||
@@ -620,6 +677,8 @@
|
||||
"unlockByTriggers": () => triggerStore.unlockByTriggers,
|
||||
"setDisabledByQuery": () => triggerStore.setDisabledByQuery,
|
||||
"setDisabledByTriggers": () => triggerStore.setDisabledByTriggers,
|
||||
"deleteByQuery": () => triggerStore.deleteByQuery,
|
||||
"deleteByTriggers": () => triggerStore.deleteByTriggers,
|
||||
};
|
||||
|
||||
if (queryBulkAction.value) {
|
||||
@@ -752,86 +811,86 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.data-table-wrapper {
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.backfillContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.trigger-issue-icon {
|
||||
color: var(--ks-content-warning);
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.alert-circle-icon {
|
||||
color: var(--ks-content-warning);
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon) {
|
||||
pointer-events: none;
|
||||
|
||||
.el-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-switch) {
|
||||
.is-text {
|
||||
padding: 0 3px;
|
||||
color: inherit;
|
||||
.data-table-wrapper {
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.backfillContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.trigger-issue-icon {
|
||||
color: var(--ks-content-warning);
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.alert-circle-icon {
|
||||
color: var(--ks-content-warning);
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon) {
|
||||
pointer-events: none;
|
||||
|
||||
.el-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-switch) {
|
||||
.is-text {
|
||||
color: #ffffff;
|
||||
padding: 0 3px;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.is-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
a {
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
}
|
||||
|
||||
.wide-tooltip {
|
||||
max-width: 400px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
color: var(--ks-content-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
background: var(--bs-gray-100);
|
||||
|
||||
.el-collapse-item__header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
font-size: var(--bs-font-size-sm);
|
||||
.el-table {
|
||||
a {
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__content {
|
||||
.wide-tooltip {
|
||||
max-width: 400px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
color: var(--ks-content-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border-radius: var(--bs-border-radius-lg);
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
background: var(--bs-gray-100);
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
.el-collapse-item__header,
|
||||
.el-collapse-item__content {
|
||||
&:last-child {
|
||||
border-bottom-left-radius: var(--bs-border-radius-lg);
|
||||
border-bottom-right-radius: var(--bs-border-radius-lg);
|
||||
.el-collapse-item__header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
font-size: var(--bs-font-size-sm);
|
||||
}
|
||||
|
||||
.el-collapse-item__content {
|
||||
background: var(--bs-gray-100);
|
||||
border-bottom: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
.el-collapse-item__header,
|
||||
.el-collapse-item__content {
|
||||
&:last-child {
|
||||
border-bottom-left-radius: var(--bs-border-radius-lg);
|
||||
border-bottom-right-radius: var(--bs-border-radius-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -183,4 +183,4 @@ export function useExecutionRoot() {
|
||||
getBaseTabs,
|
||||
setupLifecycle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ interface TriggerBulkOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface TriggerDeleteOptions {
|
||||
namespace: string;
|
||||
flowId: string;
|
||||
triggerId: string;
|
||||
}
|
||||
|
||||
export const useTriggerStore = defineStore("trigger", {
|
||||
state: () => ({}),
|
||||
|
||||
@@ -132,6 +138,21 @@ export const useTriggerStore = defineStore("trigger", {
|
||||
async setDisabledByTriggers(options: TriggerBulkOptions) {
|
||||
const response = await this.$http.post(`${apiUrl()}/triggers/set-disabled/by-triggers`, options);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
|
||||
async delete(options: TriggerDeleteOptions) {
|
||||
const response = await this.$http.delete(`${apiUrl()}/triggers/${options.namespace}/${options.flowId}/${options.triggerId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteByQuery(options: TriggerBulkOptions) {
|
||||
const response = await this.$http.post(`${apiUrl()}/triggers/delete/by-query`, null, {params: options});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteByTriggers(options: TriggerBulkOptions) {
|
||||
const response = await this.$http.post(`${apiUrl()}/triggers/delete/by-triggers`, options);
|
||||
return response.data;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1835,6 +1835,13 @@
|
||||
"search_blueprints": "Search blueprints",
|
||||
"search_plugins": "Search {count}+ plugins"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete trigger": "Delete trigger",
|
||||
"delete triggers": "Delete triggers",
|
||||
"bulk delete triggers": "Are you sure you want to delete {count} triggers?",
|
||||
"bulk success delete triggers": "{count} triggers have been deleted successfully",
|
||||
"delete trigger confirmation": "Are you sure you want to delete trigger {id}? WARNING: deleting a trigger may lead to duplicate executions if the trigger is still active in a flow",
|
||||
"delete trigger success": "Trigger {id} has been deleted successfully",
|
||||
"delete trigger error": "Error deleting trigger {id}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const render: Story["render"] = ({modelValue}) => ({
|
||||
fontSize: "12px",
|
||||
textAlign: "right",
|
||||
padding: "0 1rem"
|
||||
};
|
||||
};
|
||||
|
||||
return () => <div style="padding: 1rem;border: 1px solid var(--ks-border-primary); border-radius: 4px; margin: 1rem; background: var(--ks-background-body)">
|
||||
<div style={{...labelStyle, background: "red", width: "250px"}}>This is an example of 250px wide element.</div>
|
||||
@@ -280,8 +280,8 @@ export const TabReorderTest: Story = {
|
||||
// Perform drop operation at the calculated position
|
||||
await fireEvent.drop(panelOverlay);
|
||||
|
||||
// Wait for the reorder to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// Wait for the reorder to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(canvas.getAllByRole("tab").map(tab => tab.querySelector(".tab-title")?.textContent?.trim())).toMatchObject(["Tab 3", "Tab 1", "Tab 2"]);
|
||||
}
|
||||
@@ -346,8 +346,8 @@ export const TabMoveBetweenPanelsTest: Story = {
|
||||
// Perform drop operation at the calculated position
|
||||
fireEvent.drop(panelOverlay);
|
||||
|
||||
// Wait for the reorder to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// Wait for the reorder to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Verify the tabs have been reordered
|
||||
expect(
|
||||
@@ -383,4 +383,4 @@ export const SplitPanel: Story = {
|
||||
|
||||
expect(canvas.getAllByRole("tablist")).toHaveLength(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,6 +507,82 @@ public class TriggerController {
|
||||
return HttpResponse.ok(BulkResponse.builder().count(count).build());
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Delete(uri = "/{namespace}/{flowId}/{triggerId}")
|
||||
@Operation(tags = {"Triggers"}, summary = "Delete a trigger")
|
||||
public HttpResponse<?> deleteTrigger(
|
||||
@Parameter(description = "The namespace") @PathVariable String namespace,
|
||||
@Parameter(description = "The flow id") @PathVariable String flowId,
|
||||
@Parameter(description = "The trigger id") @PathVariable String triggerId
|
||||
) throws HttpStatusException {
|
||||
Optional<Trigger> triggerOpt = triggerRepository.findLast(TriggerContext.builder()
|
||||
.tenantId(tenantService.resolveTenant())
|
||||
.namespace(namespace)
|
||||
.flowId(flowId)
|
||||
.triggerId(triggerId)
|
||||
.build());
|
||||
|
||||
if (triggerOpt.isEmpty()) {
|
||||
return HttpResponse.notFound();
|
||||
}
|
||||
|
||||
Trigger trigger = triggerOpt.get();
|
||||
triggerRepository.delete(trigger);
|
||||
|
||||
return HttpResponse.noContent();
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/delete/by-triggers")
|
||||
@Operation(tags = {"Triggers"}, summary = "Delete given triggers")
|
||||
public MutableHttpResponse<?> deleteTriggersByIds(
|
||||
@Parameter(description = "The triggers to delete") @Body List<Trigger> triggers
|
||||
) {
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
triggers.forEach(trigger -> {
|
||||
try {
|
||||
Optional<Trigger> triggerOpt = triggerRepository.findLast(TriggerContext.builder()
|
||||
.tenantId(tenantService.resolveTenant())
|
||||
.namespace(trigger.getNamespace())
|
||||
.flowId(trigger.getFlowId())
|
||||
.triggerId(trigger.getTriggerId())
|
||||
.build());
|
||||
|
||||
if (triggerOpt.isPresent()) {
|
||||
triggerRepository.delete(triggerOpt.get());
|
||||
count.getAndIncrement();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
});
|
||||
|
||||
return HttpResponse.ok(BulkResponse.builder().count(count.get()).build());
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/delete/by-query")
|
||||
@Operation(tags = {"Triggers"}, summary = "Delete triggers by query parameters")
|
||||
public MutableHttpResponse<?> deleteTriggersByQuery(
|
||||
@Parameter(description = "Filters") @QueryFilterFormat List<QueryFilter> filters
|
||||
|
||||
) {
|
||||
Integer count = triggerRepository
|
||||
.find(tenantService.resolveTenant(), filters)
|
||||
.map(trigger -> {
|
||||
try {
|
||||
triggerRepository.delete(trigger);
|
||||
return 1;
|
||||
} catch (Exception ignored) {
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.reduce(Integer::sum)
|
||||
.blockOptional()
|
||||
.orElse(0);
|
||||
|
||||
return HttpResponse.ok(BulkResponse.builder().count(count).build());
|
||||
}
|
||||
|
||||
@ExecuteOn(TaskExecutors.IO)
|
||||
@Post(uri = "/set-disabled/by-triggers")
|
||||
@Operation(tags = {"Triggers"}, summary = "Disable/enable given triggers")
|
||||
|
||||
@@ -27,6 +27,10 @@ import io.micronaut.reactor.http.client.ReactorHttpClient;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import java.util.Optional;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.repositories.TriggerRepositoryInterface;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -447,4 +451,163 @@ class TriggerControllerTest {
|
||||
.disabled(disabled)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Inject
|
||||
FlowRepositoryInterface flowRepositoryInterface;
|
||||
|
||||
@Inject
|
||||
TriggerRepositoryInterface triggerRepository;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testGetTrigger() {
|
||||
Flow flow = createTestFlow();
|
||||
flowRepositoryInterface.create(flow);
|
||||
|
||||
Trigger trigger = Trigger.builder()
|
||||
.triggerId("test-trigger")
|
||||
.flowId(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.flowRevision(1)
|
||||
.triggerId("test-trigger")
|
||||
.build();
|
||||
|
||||
triggerRepository.create(trigger);
|
||||
|
||||
HttpResponse<Trigger> response = client.toBlocking()
|
||||
.exchange(
|
||||
HttpRequest.GET("/triggers/" + flow.getNamespace() + "/" + flow.getId() + "/test-trigger"),
|
||||
Trigger.class
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatus());
|
||||
assertNotNull(response.body());
|
||||
assertEquals("test-trigger", response.body().getId());
|
||||
assertEquals(flow.getId(), response.body().getFlowId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTriggerNotFound() {
|
||||
HttpClientResponseException exception = assertThrows(
|
||||
HttpClientResponseException.class,
|
||||
() -> client.toBlocking().exchange(
|
||||
HttpRequest.GET("/triggers/nonexistent/flow/trigger"),
|
||||
Trigger.class
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteTriggersByQuery() {
|
||||
Flow flow = createTestFlow();
|
||||
flowRepositoryInterface.create(flow);
|
||||
|
||||
Trigger trigger = Trigger.builder()
|
||||
.id("delete-test-trigger")
|
||||
.flowId(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.flowRevision(1)
|
||||
.triggerId("delete-test-trigger")
|
||||
.build();
|
||||
|
||||
Trigger triggerByQuery1 = Trigger.builder()
|
||||
.id("query-test-trigger-1")
|
||||
.flowId(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.flowRevision(1)
|
||||
.triggerId("query-test-trigger-1")
|
||||
.build();
|
||||
|
||||
Trigger triggerByQuery2 = Trigger.builder()
|
||||
.id("query-test-trigger-2")
|
||||
.flowId(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.flowRevision(1)
|
||||
.triggerId("query-test-trigger-2")
|
||||
.build();
|
||||
|
||||
triggerRepository.save(trigger);
|
||||
triggerRepository.save(triggerByQuery1);
|
||||
triggerRepository.save(triggerByQuery2);
|
||||
|
||||
triggerRepository.create(trigger);
|
||||
|
||||
List<Trigger> allBeforeDelete = triggerRepository.findByQuery(flow.getNamespace(), flow.getId());
|
||||
assertEquals(3, allBeforeDelete.size(), "Expected 3 triggers before deletion");
|
||||
|
||||
HttpResponse<Integer> firstDeleteResponse = client.toBlocking()
|
||||
.exchange(
|
||||
HttpRequest.DELETE("/triggers/query" + "?filters[namespace][EQUALS]=" + flow.getNamespace() + "&filters[flowId][EQUALS]=" + flow.getId() + "&filters[triggerId][EQUALS]=delete-test-trigger"),
|
||||
Integer.class
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.OK, firstDeleteResponse.getStatus());
|
||||
assertEquals(1, firstDeleteResponse.body(), "Expected 1 trigger deleted");
|
||||
|
||||
List<Trigger> remainingAfterFirstDelete = triggerRepository.findByQuery(flow.getNamespace(), flow.getId());
|
||||
assertEquals(2, remainingAfterFirstDelete.size(), "Expected 2 triggers remaining after first delete");
|
||||
|
||||
HttpResponse<Integer> secondDeleteResponse = client.toBlocking()
|
||||
.exchange(
|
||||
HttpRequest.DELETE("/triggers/query" + "?filters[namespace][EQUALS]=" + flow.getNamespace() + "&filters[flowId][EQUALS]=" + flow.getId()),
|
||||
Integer.class
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.OK, secondDeleteResponse.getStatus());
|
||||
assertEquals(2, secondDeleteResponse.body(), "Expected 2 remaining triggers deleted");
|
||||
|
||||
List<Trigger> finalRemaining = triggerRepository.findByQuery(flow.getNamespace(), flow.getId());
|
||||
assertEquals(0, finalRemaining.size(), "Expected no triggers after final deletion");
|
||||
|
||||
Optional<Trigger> deletedTrigger = triggerRepository.findById(
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
"delete-test-trigger"
|
||||
);
|
||||
|
||||
assertFalse(deletedTrigger.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteTriggerById() {
|
||||
Flow flow = createTestFlow();
|
||||
flowRepositoryInterface.create(flow);
|
||||
|
||||
Trigger trigger = Trigger.builder()
|
||||
.id("delete-by-id-trigger")
|
||||
.flowId(flow.getId())
|
||||
.namespace(flow.getNamespace())
|
||||
.flowRevision(1)
|
||||
.triggerId("delete-by-id-trigger")
|
||||
.build();
|
||||
|
||||
triggerRepository.create(trigger);
|
||||
|
||||
HttpResponse<Void> response = client.toBlocking()
|
||||
.exchange(
|
||||
HttpRequest.DELETE("/triggers/" + flow.getNamespace() + "/" + flow.getId() + "/delete-by-id-trigger"),
|
||||
Void.class
|
||||
);
|
||||
|
||||
assertEquals(HttpStatus.NO_CONTENT, response.getStatus());
|
||||
|
||||
Optional<Trigger> deletedTrigger = triggerRepository.findById(
|
||||
flow.getNamespace(),
|
||||
flow.getId(),
|
||||
"delete-by-id-trigger"
|
||||
);
|
||||
|
||||
assertFalse(deletedTrigger.isPresent());
|
||||
}
|
||||
|
||||
private Flow createTestFlow() {
|
||||
return Flow.builder()
|
||||
.id("trigger-test-flow")
|
||||
.namespace("io.kestra.tests")
|
||||
.revision(1)
|
||||
.tasks(List.of())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user