mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
1 Commits
dependabot
...
test/merge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2dfd0d9bc |
@@ -41,7 +41,9 @@
|
|||||||
<template #expand>
|
<template #expand>
|
||||||
<el-table-column type="expand">
|
<el-table-column type="expand">
|
||||||
<template #default="props">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,6 +73,9 @@
|
|||||||
<el-button @click="deleteBackfills()">
|
<el-button @click="deleteBackfills()">
|
||||||
{{ $t("delete backfills") }}
|
{{ $t("delete backfills") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="deleteTriggers()" type="danger">
|
||||||
|
{{ $t("delete triggers") }}
|
||||||
|
</el-button>
|
||||||
</BulkSelect>
|
</BulkSelect>
|
||||||
</template>
|
</template>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -95,17 +100,23 @@
|
|||||||
:sortOrders="['flowId', 'namespace', 'nextExecutionDate'].includes(col.prop) ? ['ascending', 'descending'] : undefined"
|
:sortOrders="['flowId', 'namespace', 'nextExecutionDate'].includes(col.prop) ? ['ascending', 'descending'] : undefined"
|
||||||
>
|
>
|
||||||
<template #header v-if="col.prop === 'date'">
|
<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>
|
<span>{{ col.label }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #header v-else-if="col.prop === 'updatedDate'">
|
<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>
|
<span>{{ col.label }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #header v-else-if="col.prop === 'nextExecutionDate'">
|
<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>
|
<span>{{ col.label }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
@@ -181,13 +192,24 @@
|
|||||||
<LockOff />
|
<LockOff />
|
||||||
</Kicon>
|
</Kicon>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button>
|
||||||
|
<Kicon
|
||||||
|
:tooltip="$t('delete trigger')"
|
||||||
|
placement="left"
|
||||||
|
@click="confirmDeleteTrigger(scope.row)"
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</Kicon>
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :label="$t('backfill')" columnKey="backfill">
|
<el-table-column :label="$t('backfill')" columnKey="backfill">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<div class="backfillContainer items-center gap-2">
|
<div class="backfillContainer items-center gap-2">
|
||||||
<span v-if="scope.row.backfill" class="statusIcon">
|
<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 />
|
<PlayBox font />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip v-else :content="$t('backfill paused')">
|
<el-tooltip v-else :content="$t('backfill paused')">
|
||||||
@@ -297,7 +319,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import _merge from "lodash/merge";
|
import _merge from "lodash/merge";
|
||||||
import {ref, computed, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
@@ -311,18 +333,16 @@
|
|||||||
import {useTriggerFilter} from "../filter/configurations";
|
import {useTriggerFilter} from "../filter/configurations";
|
||||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
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 action from "../../models/action";
|
||||||
import permission from "../../models/permission";
|
import permission from "../../models/permission";
|
||||||
|
|
||||||
const triggerFilter = useTriggerFilter();
|
|
||||||
|
|
||||||
import LockOff from "vue-material-design-icons/LockOff.vue";
|
import LockOff from "vue-material-design-icons/LockOff.vue";
|
||||||
import PlayBox from "vue-material-design-icons/PlayBox.vue";
|
import PlayBox from "vue-material-design-icons/PlayBox.vue";
|
||||||
import PauseBox from "vue-material-design-icons/PauseBox.vue";
|
import PauseBox from "vue-material-design-icons/PauseBox.vue";
|
||||||
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
|
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
|
||||||
import CalendarCollapseHorizontalOutline from "vue-material-design-icons/CalendarCollapseHorizontalOutline.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 Id from "../Id.vue";
|
||||||
import Kicon from "../Kicon.vue";
|
import Kicon from "../Kicon.vue";
|
||||||
@@ -341,6 +361,8 @@
|
|||||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||||
import useRouteContext from "../../composables/useRouteContext";
|
import useRouteContext from "../../composables/useRouteContext";
|
||||||
|
|
||||||
|
const triggerFilter = useTriggerFilter();
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -601,9 +623,44 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const genericConfirmAction = (toastKey: string, queryAction: string, byIdAction: string, success: string, data?: any) => {
|
const confirmDeleteTrigger = (trigger) => {
|
||||||
toast.confirm(
|
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)
|
() => genericConfirmCallback(queryAction, byIdAction, success, data)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -620,6 +677,8 @@
|
|||||||
"unlockByTriggers": () => triggerStore.unlockByTriggers,
|
"unlockByTriggers": () => triggerStore.unlockByTriggers,
|
||||||
"setDisabledByQuery": () => triggerStore.setDisabledByQuery,
|
"setDisabledByQuery": () => triggerStore.setDisabledByQuery,
|
||||||
"setDisabledByTriggers": () => triggerStore.setDisabledByTriggers,
|
"setDisabledByTriggers": () => triggerStore.setDisabledByTriggers,
|
||||||
|
"deleteByQuery": () => triggerStore.deleteByQuery,
|
||||||
|
"deleteByTriggers": () => triggerStore.deleteByTriggers,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (queryBulkAction.value) {
|
if (queryBulkAction.value) {
|
||||||
@@ -752,86 +811,86 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.data-table-wrapper {
|
.data-table-wrapper {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
padding-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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.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 {
|
.is-text {
|
||||||
color: #ffffff;
|
padding: 0 3px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked {
|
||||||
|
.is-text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.el-table {
|
.el-table {
|
||||||
a {
|
a {
|
||||||
color: var(--ks-content-link);
|
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-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);
|
background: var(--bs-gray-100);
|
||||||
border-bottom: 1px solid var(--ks-border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-collapse-item__header,
|
.el-collapse-item__header {
|
||||||
.el-collapse-item__content {
|
background: transparent;
|
||||||
&:last-child {
|
border-bottom: 1px solid var(--ks-border-primary);
|
||||||
border-bottom-left-radius: var(--bs-border-radius-lg);
|
font-size: var(--bs-font-size-sm);
|
||||||
border-bottom-right-radius: var(--bs-border-radius-lg);
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
@@ -36,6 +36,12 @@ interface TriggerBulkOptions {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TriggerDeleteOptions {
|
||||||
|
namespace: string;
|
||||||
|
flowId: string;
|
||||||
|
triggerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const useTriggerStore = defineStore("trigger", {
|
export const useTriggerStore = defineStore("trigger", {
|
||||||
state: () => ({}),
|
state: () => ({}),
|
||||||
|
|
||||||
@@ -132,6 +138,21 @@ export const useTriggerStore = defineStore("trigger", {
|
|||||||
async setDisabledByTriggers(options: TriggerBulkOptions) {
|
async setDisabledByTriggers(options: TriggerBulkOptions) {
|
||||||
const response = await this.$http.post(`${apiUrl()}/triggers/set-disabled/by-triggers`, options);
|
const response = await this.$http.post(`${apiUrl()}/triggers/set-disabled/by-triggers`, options);
|
||||||
return response.data;
|
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_blueprints": "Search blueprints",
|
||||||
"search_plugins": "Search {count}+ plugins"
|
"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",
|
fontSize: "12px",
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
padding: "0 1rem"
|
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)">
|
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>
|
<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
|
// Perform drop operation at the calculated position
|
||||||
await fireEvent.drop(panelOverlay);
|
await fireEvent.drop(panelOverlay);
|
||||||
|
|
||||||
// Wait for the reorder to complete
|
// Wait for the reorder to complete
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
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"]);
|
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
|
// Perform drop operation at the calculated position
|
||||||
fireEvent.drop(panelOverlay);
|
fireEvent.drop(panelOverlay);
|
||||||
|
|
||||||
// Wait for the reorder to complete
|
// Wait for the reorder to complete
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// Verify the tabs have been reordered
|
// Verify the tabs have been reordered
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -507,6 +507,82 @@ public class TriggerController {
|
|||||||
return HttpResponse.ok(BulkResponse.builder().count(count).build());
|
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)
|
@ExecuteOn(TaskExecutors.IO)
|
||||||
@Post(uri = "/set-disabled/by-triggers")
|
@Post(uri = "/set-disabled/by-triggers")
|
||||||
@Operation(tags = {"Triggers"}, summary = "Disable/enable given 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 jakarta.inject.Inject;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
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.Duration;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@@ -447,4 +451,163 @@ class TriggerControllerTest {
|
|||||||
.disabled(disabled)
|
.disabled(disabled)
|
||||||
.build();
|
.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