Files
kestra/ui/src/components/executions/Executions.vue
2025-09-10 22:44:21 +02:00

1140 lines
49 KiB
Vue

<template>
<TopNavBar v-if="topbar" :title="routeInfo.title">
<template #additional-right v-if="displayButtons">
<ul>
<template v-if="$route.name === 'executions/list'">
<li>
<template v-if="hasAnyExecute">
<TriggerFlow />
</template>
</li>
</template>
<template v-if="$route.name === 'flows/update'">
<li>
<template v-if="isAllowedEdit">
<el-button :icon="Pencil" size="large" @click="editFlow" :disabled="isReadOnly">
{{ $t("edit flow") }}
</el-button>
</template>
</li>
<li>
<TriggerFlow
v-if="flowStore.flow"
:disabled="flowStore.flow.disabled || isReadOnly"
:flowId="flowStore.flow.id"
:namespace="flowStore.flow.namespace"
/>
</li>
</template>
</ul>
</template>
</TopNavBar>
<section data-component="FILENAME_PLACEHOLDER" :class="{'container padding-bottom': topbar}" v-if="ready">
<DataTable
@page-changed="onPageChanged"
ref="dataTable"
:total="executionsStore.total"
:size="pageSize"
:page="pageNumber"
:embed="embed"
>
<template #navbar v-if="isDisplayedTop">
<KestraFilter
prefix="executions"
:language="namespace === undefined || flowId === undefined ? ExecutionFilterLanguage : FlowExecutionFilterLanguage"
:buttons="{
refresh: {shown: true, callback: refresh},
settings: {shown: true, charts: {shown: true, value: showChart, callback: onShowChartChange}}
}"
:propertiesWidth="182"
:properties="{
shown: true,
columns: optionalColumns,
displayColumns,
storageKey: 'executions'
}"
@update-properties="updateDisplayColumns"
/>
</template>
<template v-if="showStatChart()" #top>
<Sections ref="dashboardComponent" :dashboard="{id: 'default'}" :charts showDefault />
</template>
<template #table>
<SelectTable
ref="selectTable"
:data="executionsStore.executions"
:defaultSort="{prop: 'state.startDate', order: 'descending'}"
tableLayout="auto"
fixed
@row-dblclick="row => onRowDoubleClick(executionParams(row))"
@sort-change="onSort"
@selection-change="handleSelectionChange"
:selectable="!hidden?.includes('selection') && canCheck"
:no-data-text="$t('no_results.executions')"
>
<template #select-actions>
<BulkSelect
:selectAll="queryBulkAction"
:selections="selection"
:total="executionsStore.total"
@update:select-all="toggleAllSelection"
@unselect="toggleAllUnselected"
>
<!-- Always visible buttons -->
<el-button v-if="canUpdate" :icon="StateMachine" @click="changeStatusDialogVisible = !changeStatusDialogVisible">
{{ $t("change state") }}
</el-button>
<el-button v-if="canUpdate" :icon="Restart" @click="restartExecutions()">
{{ $t("restart") }}
</el-button>
<el-button v-if="canCreate" :icon="PlayBoxMultiple" @click="isOpenReplayModal = !isOpenReplayModal">
{{ $t("replay") }}
</el-button>
<el-button v-if="canUpdate" :icon="StopCircleOutline" @click="killExecutions()">
{{ $t("kill") }}
</el-button>
<el-button v-if="canDelete" :icon="Delete" @click="deleteExecutions()">
{{ $t("delete") }}
</el-button>
<!-- Dropdown with additional actions -->
<el-dropdown>
<el-button>
<DotsVertical />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="canUpdate" :icon="LabelMultiple" @click=" isOpenLabelsModal = !isOpenLabelsModal">
{{ $t("Set labels") }}
</el-dropdown-item>
<el-dropdown-item v-if="canUpdate" :icon="PlayBox" @click="resumeExecutions()">
{{ $t("resume") }}
</el-dropdown-item>
<el-dropdown-item v-if="canUpdate" :icon="PauseBox" @click="pauseExecutions()">
{{ $t("pause") }}
</el-dropdown-item>
<el-dropdown-item v-if="canUpdate" :icon="QueueFirstInLastOut" @click="unqueueDialogVisible = true">
{{ $t("unqueue") }}
</el-dropdown-item>
<el-dropdown-item v-if="canUpdate" :icon="RunFast" @click="forceRunExecutions()">
{{ $t("force run") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</BulkSelect>
<el-dialog
v-if="isOpenLabelsModal"
v-model="isOpenLabelsModal"
destroyOnClose
:appendToBody="true"
alignCenter
>
<template #header>
<h5>{{ $t("Set labels") }}</h5>
</template>
<template #footer>
<el-button @click="isOpenLabelsModal = false">
{{ $t("cancel") }}
</el-button>
<el-button type="primary" @click="setLabels()">
{{ $t("ok") }}
</el-button>
</template>
<el-form>
<ElFormItem :label="$t('execution labels')">
<LabelInput
:key="executionLabels"
v-model:labels="executionLabels"
/>
</ElFormItem>
</el-form>
</el-dialog>
</template>
<template #default>
<el-table-column
prop="id"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('id')"
>
<template #default="scope">
<RouterLink
:to="{
name: 'executions/update',
params: {
namespace: scope.row.namespace,
flowId: scope.row.flowId,
id: scope.row.id
}
}"
class="execution-id"
>
<Id :value="scope.row.id" :shrink="true" />
</RouterLink>
</template>
</el-table-column>
<el-table-column
prop="state.startDate"
v-if="displayColumn('state.startDate')"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('start date')"
>
<template #default="scope">
<DateAgo :inverted="true" :date="scope.row.state.startDate" />
</template>
</el-table-column>
<el-table-column
prop="state.endDate"
v-if="displayColumn('state.endDate')"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('end date')"
>
<template #default="scope">
<DateAgo :inverted="true" :date="scope.row.state.endDate" />
</template>
</el-table-column>
<el-table-column
prop="state.duration"
v-if="displayColumn('state.duration')"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('duration')"
>
<template #default="scope">
<span v-if="isRunning(scope.row)">{{
$filters.humanizeDuration(durationFrom(scope.row))
}}</span>
<span v-else>{{ $filters.humanizeDuration(scope.row.state.duration) }}</span>
</template>
</el-table-column>
<el-table-column
v-if="$route.name !== 'flows/update' && displayColumn('namespace')"
prop="namespace"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('namespace')"
:formatter="(_, __, cellValue) => $filters.invisibleSpace(cellValue)"
/>
<el-table-column
v-if="$route.name !== 'flows/update' && displayColumn('flowId')"
prop="flowId"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('flow')"
>
<template #default="scope">
<router-link
:to="{name: 'flows/update', params: {namespace: scope.row.namespace, id: scope.row.flowId}}"
>
{{ $filters.invisibleSpace(scope.row.flowId) }}
</router-link>
</template>
</el-table-column>
<el-table-column v-if="displayColumn('labels')" :label="$t('labels')">
<template #default="scope">
<Labels :labels="filteredLabels(scope.row.labels)" />
</template>
</el-table-column>
<el-table-column
prop="state.current"
v-if="displayColumn('state.current')"
sortable="custom"
:sortOrders="['ascending', 'descending']"
:label="$t('state')"
>
<template #default="scope">
<Status :status="scope.row.state.current" size="small" />
</template>
</el-table-column>
<el-table-column
prop="flowRevision"
v-if="displayColumn('flowRevision')"
:label="$t('revision')"
className="shrink"
>
<template #default="scope">
<code class="code-text">{{ scope.row.flowRevision }}</code>
</template>
</el-table-column>
<el-table-column
prop="inputs"
v-if="displayColumn('inputs')"
:label="$t('inputs')"
align="center"
>
<template #default="scope">
<el-tooltip effect="light">
<template #content>
<pre class="mb-0">{{ JSON.stringify(scope.row.inputs, null, "\t") }}</pre>
</template>
<div>
<Import v-if="scope.row.inputs" class="fs-5" />
</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
prop="taskRunList.taskId"
v-if="displayColumn('taskRunList.taskId')"
:label="$t('task id')"
>
<template #header="scope">
<el-tooltip :content="$t('taskid column details')" effect="light">
{{ scope.column.label }}
</el-tooltip>
</template>
<template #default="scope">
<code class="code-text">
{{ scope.row.taskRunList?.slice(-1)[0].taskId }}
{{
scope.row.taskRunList?.slice(-1)[0].attempts?.length > 1 ? `(${scope.row.taskRunList?.slice(-1)[0].attempts.length})` : ""
}}
</code>
</template>
</el-table-column>
<el-table-column
columnKey="action"
className="row-action"
:label="$t('actions')"
>
<template #default="scope">
<router-link
:to="{name: 'executions/update', params: {namespace: scope.row.namespace, flowId: scope.row.flowId, id: scope.row.id}, query: {revision: scope.row.flowRevision}}"
>
<Kicon :tooltip="$t('details')" placement="left">
<TextSearch />
</Kicon>
</router-link>
</template>
</el-table-column>
</template>
</SelectTable>
</template>
</DataTable>
</section>
<el-dialog v-if="changeStatusDialogVisible" v-model="changeStatusDialogVisible" :id="Utils.uid()" destroyOnClose :appendToBody="true" alignCenter>
<template #header>
<h5>{{ $t("confirmation") }}</h5>
</template>
<template #default>
<p v-html="changeStatusToast()" />
<el-select
:required="true"
v-model="selectedStatus"
:persistent="false"
>
<el-option
v-for="item in states"
:key="item.code"
:value="item.code"
>
<template #default>
<Status size="small" :label="false" class="me-1" :status="item.code" />
<span v-html="item.label" />
</template>
</el-option>
</el-select>
</template>
<template #footer>
<el-button @click="changeStatusDialogVisible = false">
{{ $t('cancel') }}
</el-button>
<el-button
type="primary"
@click="changeStatus()"
>
{{ $t('ok') }}
</el-button>
</template>
</el-dialog>
<el-dialog v-if="unqueueDialogVisible" v-model="unqueueDialogVisible" destroyOnClose :appendToBody="true">
<template #header>
<h5>{{ $t("confirmation") }}</h5>
</template>
<template #default>
<p v-html="$t('unqueue title multiple', {count: queryBulkAction ? executionsStore.total : selection.length})" />
<el-select
:required="true"
v-model="selectedStatus"
:persistent="false"
>
<el-option
v-for="item in unQueuestates"
:key="item.code"
:value="item.code"
>
<template #default>
<Status size="small" :label="false" class="me-1" :status="item.code" />
<span v-html="item.label" />
</template>
</el-option>
</el-select>
</template>
<template #footer>
<el-button @click="unqueueDialogVisible = false">
{{ $t('cancel') }}
</el-button>
<el-button
type="primary"
@click="unqueueExecutions()"
>
{{ $t('ok') }}
</el-button>
</template>
</el-dialog>
<el-dialog v-if="isOpenReplayModal" v-model="isOpenReplayModal" :id="Utils.uid()" destroyOnClose :appendToBody="true" alignCenter>
<template #header>
<h5>{{ $t("confirmation") }}</h5>
</template>
<template #default>
<p v-html="changeReplayToast()" />
</template>
<template #footer>
<el-button @click="isOpenReplayModal = false">
{{ $t('cancel') }}
</el-button>
<el-button @click="replayExecutions(true)">
{{ $t('replay latest revision') }}
</el-button>
<el-button
type="primary"
@click="replayExecutions(false)"
>
{{ $t('ok') }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import BulkSelect from "../layout/BulkSelect.vue";
import SelectTable from "../layout/SelectTable.vue";
import PlayBox from "vue-material-design-icons/PlayBox.vue";
import PlayBoxMultiple from "vue-material-design-icons/PlayBoxMultiple.vue";
import DotsVertical from "vue-material-design-icons/DotsVertical.vue";
import Restart from "vue-material-design-icons/Restart.vue";
import Delete from "vue-material-design-icons/Delete.vue";
import StopCircleOutline from "vue-material-design-icons/StopCircleOutline.vue";
import Pencil from "vue-material-design-icons/Pencil.vue";
import Import from "vue-material-design-icons/Import.vue";
import LabelMultiple from "vue-material-design-icons/LabelMultiple.vue";
import StateMachine from "vue-material-design-icons/StateMachine.vue";
import PauseBox from "vue-material-design-icons/PauseBox.vue";
import KestraFilter from "../filter/KestraFilter.vue"
import QueueFirstInLastOut from "vue-material-design-icons/QueueFirstInLastOut.vue";
import RunFast from "vue-material-design-icons/RunFast.vue";
import ExecutionFilterLanguage from "../../composables/monaco/languages/filters/impl/executionFilterLanguage.ts";
import FlowExecutionFilterLanguage from "../../composables/monaco/languages/filters/impl/flowExecutionFilterLanguage.js";
import Sections from "../dashboard/sections/Sections.vue";
</script>
<script>
import {mapStores} from "pinia";
import {useMiscStore} from "override/stores/misc.ts";
import DataTable from "../layout/DataTable.vue";
import TextSearch from "vue-material-design-icons/TextSearch.vue";
import Status from "../Status.vue";
import RouteContext from "../../mixins/routeContext";
import TopNavBar from "../../components/layout/TopNavBar.vue";
import DataTableActions from "../../mixins/dataTableActions";
import SelectTableActions from "../../mixins/selectTableActions";
import Kicon from "../Kicon.vue"
import Labels from "../layout/Labels.vue"
import RestoreUrl from "../../mixins/restoreUrl";
import {State} from "@kestra-io/ui-libs"
import Id from "../Id.vue";
import _merge from "lodash/merge";
import permission from "../../models/permission";
import action from "../../models/action";
import TriggerFlow from "../../components/flows/TriggerFlow.vue";
import {storageKeys} from "../../utils/constants";
import LabelInput from "../../components/labels/LabelInput.vue";
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
import {h, ref} from "vue";
import DateAgo from "../layout/DateAgo.vue";
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
import YAML_CHART from "../dashboard/assets/executions_timeseries_chart.yaml?raw";
import Utils from "../../utils/utils";
import {filterLabels} from "./utils"
import {useExecutionsStore} from "../../stores/executions";
import {useAuthStore} from "override/stores/auth.ts";
import {useFlowStore} from "../../stores/flow.ts";
import {defaultNamespace} from "../../composables/useNamespaces";
export default {
mixins: [RouteContext, RestoreUrl, DataTableActions, SelectTableActions],
components: {
Status,
TextSearch,
DataTable,
Kicon,
Labels,
Id,
TriggerFlow,
TopNavBar,
LabelInput,
DateAgo
},
emits: ["state-count"],
props: {
hidden: {
type: Array,
default: null
},
statuses: {
type: Array,
default: () => []
},
isReadOnly: {
type: Boolean,
default: false
},
embed: {
type: Boolean,
default: false
},
topbar: {
type: Boolean,
default: true
},
filter: {
type: Boolean,
default: true
},
namespace: {
type: String,
required: false,
default: undefined
},
flowId: {
type: String,
required: false,
default: undefined
},
isConcurrency: {
type: Boolean,
default: false
},
id: {
type: String,
required: false,
default: null,
},
visibleCharts: {
type: Boolean,
default: false
},
},
data() {
return {
isDefaultNamespaceAllow: true,
dblClickRouteName: "executions/update",
flowTriggerDetails: undefined,
recomputeInterval: false,
showChart: ["true", null].includes(localStorage.getItem(storageKeys.SHOW_CHART)),
optionalColumns: [
{
label: this.$t("start date"),
prop: "state.startDate",
default: true
},
{
label: this.$t("end date"),
prop: "state.endDate",
default: true
},
{
label: this.$t("duration"),
prop: "state.duration",
default: true
},
{
label: this.$t("namespace"),
prop: "namespace",
default: true
},
{
label: this.$t("flow"),
prop: "flowId",
default: true
},
{
label: this.$t("labels"),
prop: "labels",
default: true
},
{
label: this.$t("state"),
prop: "state.current",
default: true
},
{
label: this.$t("revision"),
prop: "flowRevision",
default: false
},
{
label: this.$t("inputs"),
prop: "inputs",
default: false
},
{
label: this.$t("task id"),
prop: "taskRunList.taskId",
default: false
}
],
displayColumns: [],
storageKey: storageKeys.DISPLAY_EXECUTIONS_COLUMNS,
isOpenLabelsModal: false,
executionLabels: [],
actionOptions: {},
lastRefreshDate: new Date(),
isOpenReplayModal: false,
changeStatusDialogVisible: false,
unqueueDialogVisible: false,
selectedStatus: undefined,
loading: false
};
},
created() {
// allow to have different storage key for flow executions list
if (this.$route.name === "flows/update") {
this.storageKey = storageKeys.DISPLAY_FLOW_EXECUTIONS_COLUMNS;
this.optionalColumns = this.optionalColumns.filter(col => col.prop !== "namespace" && col.prop !== "flowId")
}
this.displayColumns = localStorage.getItem("columns_executions")?.split(",")
|| this.optionalColumns.filter(col => col.default).map(col => col.prop);
},
computed: {
...mapStores(useMiscStore, useExecutionsStore, useFlowStore, useAuthStore),
routeInfo() {
return {
title: this.$t("executions")
};
},
endDate() {
if (this.$route.query.endDate) {
return this.$route.query.endDate;
}
return undefined;
},
startDate() {
if (this.$route.query.startDate && this.lastRefreshDate) {
return this.$route.query.startDate;
}
if (this.$route.query.timeRange) {
return this.$moment().subtract(this.$moment.duration(this.$route.query.timeRange).as("milliseconds")).toISOString(true);
}
// the default is PT30D
return this.$moment().subtract(30, "days").toISOString(true);
},
displayButtons() {
return (this.$route.name === "flows/update") || (this.$route.name === "executions/list");
},
canCheck() {
return this.canDelete || this.canUpdate;
},
canCreate() {
return this.authStore.user?.isAllowed(permission.EXECUTION, action.CREATE, this.namespace);
},
canUpdate() {
return this.authStore.user?.isAllowed(permission.EXECUTION, action.UPDATE, this.namespace);
},
canDelete() {
return this.authStore.user?.isAllowed(permission.EXECUTION, action.DELETE, this.namespace);
},
isAllowedEdit() {
return this.authStore.user?.isAllowed(permission.FLOW, action.UPDATE, this.flowStore.flow.namespace);
},
hasAnyExecute() {
return this.authStore.user?.hasAnyActionOnAnyNamespace(permission.EXECUTION, action.CREATE);
},
isDisplayedTop() {
if(this.visibleCharts) return true;
else return this.embed === false && this.filter
},
states() {
return [ State.FAILED, State.SUCCESS, State.WARNING, State.CANCELLED,].map(value => {
return {
code: value,
label: this.$t("mark as", {status: value})
};
});
},
unQueuestates() {
return [State.RUNNING, State.CANCELLED, State.FAILED].map(value => ({
code: value,
label: this.$t("unqueue as", {status: value}),
}));
},
executionsCount() {
return [...this.daily].reduce((a, b) => {
return a + Object.values(b.executionCounts).reduce((a, b) => a + b, 0);
}, 0);
},
selectedNamespace(){
return this.namespace !== null && this.namespace !== undefined ? this.namespace : this.$route.query?.namespace;
},
charts() {
return [
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
];
}
},
beforeRouteEnter(to, _, next) {
const query = {...to.query};
let queryHasChanged = false;
const queryKeys = Object.keys(query);
if (this?.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
query["filters[namespace][PREFIX]"] = defaultNamespace();
queryHasChanged = true;
}
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
query["filters[scope][EQUALS]"] = "USER";
queryHasChanged = true;
}
if (queryHasChanged) {
next({
...to,
query,
replace: true
});
} else {
next();
}
},
methods: {
filteredLabels(labels) {
const toIgnore = this.miscStore.configs?.hiddenLabelsPrefixes || [];
// Extract only the keys from the route query labels
const allowedLabels = this.$route.query.labels ? this.$route.query.labels.map(label => label.split(":")[0]) : [];
return labels?.filter(label => {
// Check if the label key matches any prefix but allow it if it's in the query
return !toIgnore.some(prefix => label.key.startsWith(prefix)) || allowedLabels.includes(label.key);
});
},
executionParams(row) {
return {
namespace: row.namespace,
flowId: row.flowId,
id: row.id
}
},
onDisplayColumnsChange(event) {
localStorage.setItem(this.storageKey, event);
this.displayColumns = event;
},
displayColumn(column) {
return this.hidden ? !this.hidden.includes(column) : this.displayColumns.includes(column);
},
updateDisplayColumns(newColumns) {
this.displayColumns = newColumns;
},
onShowChartChange(value) {
this.showChart = value;
localStorage.setItem(storageKeys.SHOW_CHART, value);
},
showStatChart() {
return this.isDisplayedTop && this.showChart;
},
refresh() {
this.recomputeInterval = !this.recomputeInterval;
this.$refs.dashboardComponent.refreshCharts();
this.load();
},
selectionMapper(execution) {
return execution.id
},
isRunning(item) {
return State.isRunning(item.state.current);
},
onStatusChange() {
this.load(this.onDataLoaded);
},
loadQuery(base) {
let queryFilter = this.queryWithFilter();
if (this.namespace) {
queryFilter["filters[namespace][PREFIX]"] = this.namespace;
}
if (this.flowId) {
queryFilter["filters[flowId][EQUALS]"] = this.flowId;
}
const hasStateFilters = Object.keys(queryFilter).some(key => key.startsWith("filters[state]")) || queryFilter.state;
if (!hasStateFilters && this.statuses?.length > 0) {
queryFilter["filters[state][IN]"] = this.statuses.join(",");
}
return _merge(base, queryFilter)
},
loadData(callback) {
this.lastRefreshDate = new Date();
this.executionsStore.findExecutions(this.loadQuery({
size: parseInt(this.$route.query.size || this.internalPageSize),
page: parseInt(this.$route.query.page || this.internalPageNumber),
sort: this.$route.query.sort || "state.startDate:desc",
state: this.$route.query.state ? [this.$route.query.state] : this.statuses
})).then(() => {
if (this.isConcurrency) {
this.emitStateCount();
}
}).finally(callback);
},
durationFrom(item) {
return (+new Date() - new Date(item.state.startDate).getTime()) / 1000
},
genericConfirmAction(toast, queryAction, byIdAction, success, showCancelButton = true) {
this.$toast().confirm(
this.$t(toast, {"executionCount": this.queryBulkAction ? this.executionsStore.total : this.selection.length}),
() => this.genericConfirmCallback(queryAction, byIdAction, success),
() => {},
showCancelButton
);
},
genericConfirmCallback(queryAction, byIdAction, success, params) {
const actionMap = {
"queryResumeExecution": () => this.executionsStore.queryResumeExecution,
"bulkResumeExecution": () => this.executionsStore.bulkResumeExecution,
"queryPauseExecution": () => this.executionsStore.queryPauseExecution,
"bulkPauseExecution": () => this.executionsStore.bulkPauseExecution,
"queryUnqueueExecution": () => this.executionsStore.queryUnqueueExecution,
"bulkUnqueueExecution": () => this.executionsStore.bulkUnqueueExecution,
"queryForceRunExecution": () => this.executionsStore.queryForceRunExecution,
"bulkForceRunExecution": () => this.executionsStore.bulkForceRunExecution,
"queryRestartExecution": () => this.executionsStore.queryRestartExecution,
"bulkRestartExecution": () => this.executionsStore.bulkRestartExecution,
"queryReplayExecution": () => this.executionsStore.queryReplayExecution,
"bulkReplayExecution": () => this.executionsStore.bulkReplayExecution,
"queryChangeExecutionStatus": () => this.executionsStore.queryChangeExecutionStatus,
"bulkChangeExecutionStatus": () => this.executionsStore.bulkChangeExecutionStatus,
"queryDeleteExecution": () => this.executionsStore.queryDeleteExecution,
"bulkDeleteExecution": () => this.executionsStore.bulkDeleteExecution,
"queryKill": () => this.executionsStore.queryKill,
"bulkKill": () => this.executionsStore.bulkKill,
};
if (this.queryBulkAction) {
const query = this.loadQuery({
sort: this.$route.query.sort || "state.startDate:desc",
state: this.$route.query.state ? [this.$route.query.state] : this.statuses,
});
let options = {...query, ...this.actionOptions};
if (params) {
options = {...options, ...params}
}
const action = actionMap[queryAction]();
return action(options)
.then(r => {
this.$toast().success(this.$t(success, {executionCount: r.data.count}));
this.loadData();
})
} else {
const selection = {executionsId: this.selection};
let options = {...selection, ...this.actionOptions};
if (params) {
options = {...options, ...params}
}
const action = actionMap[byIdAction]();
return action(options)
.then(r => {
this.$toast().success(this.$t(success, {executionCount: r.data.count}));
this.loadData();
}).catch(e => {
this.$toast().error(e?.invalids.map(exec => {
return {message: this.$t(exec.message, {executionId: exec.invalidValue})}
}), this.$t(e.message))
})
}
},
resumeExecutions() {
this.genericConfirmAction(
"bulk resume",
"queryResumeExecution",
"bulkResumeExecution",
"executions resumed",
false
);
},
pauseExecutions() {
this.genericConfirmAction(
"bulk pause",
"queryPauseExecution",
"bulkPauseExecution",
"executions paused"
);
},
unqueueExecutions() {
this.unqueueDialogVisible = false;
this.actionOptions.newStatus = this.selectedStatus;
this.genericConfirmCallback(
"queryUnqueueExecution",
"bulkUnqueueExecution",
"executions unqueue"
);
},
forceRunExecutions() {
this.genericConfirmAction(
"bulk force run",
"queryForceRunExecution",
"bulkForceRunExecution",
"executions force run"
);
},
restartExecutions() {
this.genericConfirmAction(
"bulk restart",
"queryRestartExecution",
"bulkRestartExecution",
"executions restarted"
);
},
replayExecutions(latestRevision) {
this.isOpenReplayModal = false;
this.genericConfirmCallback(
"queryReplayExecution",
"bulkReplayExecution",
"executions replayed",
{latestRevision: latestRevision}
);
},
changeReplayToast() {
return this.$t("bulk replay", {"executionCount": this.queryBulkAction ? this.executionsStore.total : this.selection.length});
},
changeStatus() {
this.changeStatusDialogVisible = false;
this.actionOptions.newStatus = this.selectedStatus;
this.genericConfirmCallback(
"queryChangeExecutionStatus",
"bulkChangeExecutionStatus",
"executions state changed"
);
},
changeStatusToast() {
return this.$t("bulk change state", {"executionCount": this.queryBulkAction ? this.executionsStore.total : this.selection.length});
},
deleteExecutions() {
const includeNonTerminated = ref(false);
const deleteLogs = ref(true);
const deleteMetrics = ref(true);
const deleteStorage = ref(true);
const message = () => h("div", null, [
h(
"p",
{innerHTML: this.$t("bulk delete", {"executionCount": this.queryBulkAction ? this.executionsStore.total : this.selection.length})}
),
h(ElFormItem, {
class: "mt-3",
label: this.$t("execution-include-non-terminated")
}, [
h(ElSwitch, {
modelValue: includeNonTerminated.value,
"onUpdate:modelValue": (val) => {
includeNonTerminated.value = val;
},
}),
]),
includeNonTerminated.value ? h(ElAlert, {
title: this.$t("execution-warn-title"),
description: this.$t("execution-warn-deleting-still-running"),
type: "warning",
showIcon: true,
closable: false,
class: "custom-warning"
}) : null,
h(ElCheckbox, {
modelValue: deleteLogs.value,
label: this.$t("execution_deletion.logs"),
"onUpdate:modelValue": (val) => (deleteLogs.value = val),
}),
h(ElCheckbox, {
modelValue: deleteMetrics.value,
label: this.$t("execution_deletion.metrics"),
"onUpdate:modelValue": (val) => (deleteMetrics.value = val),
}),
h(ElCheckbox, {
modelValue: deleteStorage.value,
label: this.$t("execution_deletion.storage"),
"onUpdate:modelValue": (val) => (deleteStorage.value = val),
}),
]);
ElMessageBox.confirm(message, this.$t("confirmation"), {
type: "confirm",
inputType: "checkbox",
inputValue: "false",
}).then(() => {
this.actionOptions.includeNonTerminated = includeNonTerminated.value;
this.actionOptions.deleteLogs = deleteLogs.value;
this.actionOptions.deleteMetrics = deleteMetrics.value;
this.actionOptions.deleteStorage = deleteStorage.value;
this.genericConfirmCallback(
"queryDeleteExecution",
"bulkDeleteExecution",
"executions deleted"
);
});
},
killExecutions() {
this.genericConfirmAction(
"bulk kill",
"queryKill",
"bulkKill",
"executions killed"
);
},
setLabels() {
const filtered = filterLabels(this.executionLabels)
if(filtered.error) {
this.$toast().error(this.$t("wrong labels"))
return;
}
this.$toast().confirm(
this.$t("bulk set labels", {"executionCount": this.queryBulkAction ? this.executionsStore.total : this.selection.length}),
() => {
if (this.queryBulkAction) {
return this.executionsStore
.querySetLabels({
params: this.loadQuery({
sort: this.$route.query.sort || "state.startDate:desc",
state: this.$route.query.state ? [this.$route.query.state] : this.statuses
}),
data: filtered.labels
})
.then(r => {
this.$toast().success(this.$t("Set labels done", {executionCount: r.data.count}));
this.loadData();
})
} else {
return this.executionsStore
.bulkSetLabels({
executionsId: this.selection,
executionLabels: filtered.labels
})
.then(r => {
this.$toast().success(this.$t("Set labels done", {executionCount: r.data.count}));
this.loadData();
}).catch(e => this.$toast().error(e.invalids.map(exec => {
return {message: this.$t(exec.message, {executionId: exec.invalidValue})}
}), this.$t(e.message)))
}
},
() => {
}
)
this.isOpenLabelsModal = false;
},
editFlow() {
this.$router.push({
name: "flows/update", params: {
namespace: this.flowStore.flow.namespace,
id: this.flowStore.flow.id,
tab: "edit",
tenant: this.$route.params.tenant
}
})
},
emitStateCount() {
const runningCount = this.executionsStore.executions.filter(execution =>
execution.state.current === State.RUNNING
)?.length;
const totalCount = this.executionsStore.total;
this.$emit("state-count", {runningCount, totalCount});
}
},
watch: {
isOpenLabelsModal(opening) {
if (opening) {
this.executionLabels = [];
}
}
},
};
</script>
<style scoped lang="scss">
.shadow {
box-shadow: 0px 2px 4px 0px var(--ks-card-shadow) !important;
}
.padding-bottom {
padding-bottom: 4rem;
}
.custom-warning {
border: 1px solid #ffb703;
border-radius: 7px;
box-shadow: 1px 1px 3px 1px #ffb703;
:deep(.el-alert__title) {
font-size: 16px;
color: #ffb703;
font-weight: bold;
}
:deep(.el-alert__description) {
font-size: 12px;
}
:deep(.el-alert__icon) {
color: #ffb703;
}
}
.code-text {
color: var(--ks-content-primary);
}
:deep(a.execution-id) code {
color: var(--bs-code-color) !important;
}
</style>