mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(executions): make prev/next buttons loop through the executions of that flow (#13296)
Closes https://github.com/kestra-io/kestra/issues/9873.
This commit is contained in:
@@ -77,18 +77,13 @@
|
||||
<div>
|
||||
{{ $t("execution replay") }}
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'executions/update',
|
||||
params: {
|
||||
...(execution.tenantId
|
||||
? {tenant: execution.tenantId}
|
||||
: {}),
|
||||
namespace: execution.namespace,
|
||||
flowId: execution.flowId,
|
||||
id: execution.originalId,
|
||||
tab: 'overview',
|
||||
},
|
||||
}"
|
||||
:to="
|
||||
createLink(
|
||||
'executions',
|
||||
execution,
|
||||
execution.originalId,
|
||||
)
|
||||
"
|
||||
>
|
||||
<Id
|
||||
:value="execution.originalId"
|
||||
@@ -164,20 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="buttons">
|
||||
<el-button @click="navigateToExecution('previous')">
|
||||
<el-icon class="el-icon--left">
|
||||
<ChevronLeft />
|
||||
</el-icon>
|
||||
{{ $t("prev_execution") }}
|
||||
</el-button>
|
||||
<el-button @click="navigateToExecution('next')">
|
||||
{{ $t("next_execution") }}
|
||||
<el-icon class="el-icon--right">
|
||||
<ChevronRight />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<PrevNext :execution />
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
@@ -191,11 +173,10 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, computed, ref} from "vue";
|
||||
|
||||
import {useRouter, useRoute} from "vue-router";
|
||||
const router = useRouter();
|
||||
import {useRoute} from "vue-router";
|
||||
const route = useRoute();
|
||||
|
||||
import {Execution, useExecutionsStore} from "../../../stores/executions";
|
||||
import {useExecutionsStore} from "../../../stores/executions";
|
||||
const store = useExecutionsStore();
|
||||
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
@@ -209,6 +190,7 @@
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import {createLink} from "./utils/links";
|
||||
import Utils from "../../../utils/utils";
|
||||
import {FilterObject} from "../../../utils/filters";
|
||||
|
||||
@@ -223,6 +205,7 @@
|
||||
import Cascader from "./components/main/Cascader.vue";
|
||||
import TriggerCascader from "./components/main/TriggerCascader.vue";
|
||||
import TimeSeries from "../../dashboard/sections/TimeSeries.vue";
|
||||
import PrevNext from "./components/main/PrevNext.vue";
|
||||
|
||||
import NoData from "../../layout/NoData.vue";
|
||||
|
||||
@@ -256,8 +239,6 @@
|
||||
import History from "vue-material-design-icons/History.vue";
|
||||
import SortVariant from "vue-material-design-icons/SortVariant.vue";
|
||||
import TimelineClockOutline from "vue-material-design-icons/TimelineClockOutline.vue";
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";
|
||||
|
||||
const emits = defineEmits(["follow"]);
|
||||
|
||||
@@ -270,32 +251,13 @@
|
||||
icon: DotsSquare,
|
||||
label: t("namespace"),
|
||||
value: execution.value.namespace,
|
||||
to: {
|
||||
name: "namespaces/update",
|
||||
params: {
|
||||
...(execution.value.tenantId
|
||||
? {tenant: execution.value.tenantId}
|
||||
: {}),
|
||||
id: execution.value.namespace,
|
||||
tab: "overview",
|
||||
},
|
||||
},
|
||||
to: createLink("namespaces", execution.value),
|
||||
},
|
||||
{
|
||||
icon: FileTreeOutline,
|
||||
label: t("flow"),
|
||||
value: execution.value.flowId,
|
||||
to: {
|
||||
name: "flows/update",
|
||||
params: {
|
||||
...(execution.value.tenantId
|
||||
? {tenant: execution.value.tenantId}
|
||||
: {}),
|
||||
namespace: execution.value.namespace,
|
||||
id: execution.value.flowId,
|
||||
tab: "overview",
|
||||
},
|
||||
},
|
||||
to: createLink("flows", execution.value),
|
||||
},
|
||||
{
|
||||
icon: LayersTripleOutline,
|
||||
@@ -397,18 +359,11 @@
|
||||
icon: History,
|
||||
label: t("parent execution"),
|
||||
value: execution.value.trigger.variables.executionId,
|
||||
to: {
|
||||
name: "executions/update",
|
||||
params: {
|
||||
...(execution.value.tenantId
|
||||
? {tenant: execution.value.tenantId}
|
||||
: {}),
|
||||
namespace: execution.value.namespace,
|
||||
flowId: execution.value.flowId,
|
||||
id: execution.value.trigger.variables.executionId,
|
||||
tab: "overview",
|
||||
},
|
||||
},
|
||||
to: createLink(
|
||||
"executions",
|
||||
execution.value,
|
||||
execution.value.trigger.variables.executionId,
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -419,18 +374,11 @@
|
||||
icon: History,
|
||||
label: t("original execution"),
|
||||
value: execution.value.originalId,
|
||||
to: {
|
||||
name: "executions/update",
|
||||
params: {
|
||||
...(execution.value.tenantId
|
||||
? {tenant: execution.value.tenantId}
|
||||
: {}),
|
||||
namespace: execution.value.namespace,
|
||||
flowId: execution.value.flowId,
|
||||
id: execution.value.originalId,
|
||||
tab: "overview",
|
||||
},
|
||||
},
|
||||
to: createLink(
|
||||
"executions",
|
||||
execution.value,
|
||||
execution.value.originalId,
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -523,51 +471,6 @@
|
||||
];
|
||||
});
|
||||
|
||||
const navigateToExecution = async (direction: "previous" | "next") => {
|
||||
if (!execution.value) return;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
namespace: execution.value.namespace,
|
||||
flowId: execution.value.flowId,
|
||||
pageSize: 100,
|
||||
sort: "state.startDate:desc",
|
||||
};
|
||||
|
||||
const response = await store.findExecutions(params);
|
||||
const result = response?.results ?? [];
|
||||
|
||||
if (!result.length) return;
|
||||
|
||||
const currentIdx = result.findIndex(
|
||||
(e: Execution) => e.id === execution.value!.id,
|
||||
);
|
||||
|
||||
if (currentIdx === -1) return;
|
||||
|
||||
// next = newer (-1), previous = older (+1)
|
||||
const targetIdx =
|
||||
direction === "previous" ? currentIdx + 1 : currentIdx - 1;
|
||||
|
||||
if (targetIdx < 0 || targetIdx >= result.length) return;
|
||||
|
||||
const target = result[targetIdx];
|
||||
|
||||
router.push({
|
||||
name: "executions/update",
|
||||
params: {
|
||||
...(target.tenantId ? {tenant: target.tenantId} : {}),
|
||||
namespace: target.namespace,
|
||||
flowId: target.flowId,
|
||||
id: target.id,
|
||||
tab: "overview",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to navigate executions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!route.params.id) return;
|
||||
loadExecution(route.params.id as string);
|
||||
@@ -710,15 +613,15 @@ $font-size-sm: $font-size-base * 0.875; // TODO: Move it into varaibles file of
|
||||
}
|
||||
}
|
||||
|
||||
#buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacer;
|
||||
& :deep(.el-empty) {
|
||||
padding: 0;
|
||||
|
||||
.el-button {
|
||||
width: calc($spacer * 12);
|
||||
font-size: $font-size-sm;
|
||||
& .el-empty__image {
|
||||
width: calc($spacer * 8) !important;
|
||||
}
|
||||
|
||||
& .el-empty__description {
|
||||
margin-top: calc($spacer / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {onMounted, computed, ref} from "vue";
|
||||
|
||||
import VarValue from "../../../VarValue.vue";
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div id="buttons">
|
||||
<el-button
|
||||
:icon="ChevronLeft"
|
||||
:disabled="prevDisabled"
|
||||
@click="navigate('previous')"
|
||||
>
|
||||
{{ $t("prev_execution") }}
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="nextDisabled" @click="navigate('next')">
|
||||
{{ $t("next_execution") }}
|
||||
<el-icon class="el-icon--right">
|
||||
<ChevronRight />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, computed, ref} from "vue";
|
||||
|
||||
import {useRouter} from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
import {
|
||||
Execution,
|
||||
useExecutionsStore,
|
||||
} from "../../../../../stores/executions";
|
||||
const store = useExecutionsStore();
|
||||
|
||||
import {createLink} from "../../utils/links";
|
||||
|
||||
import ChevronLeft from "vue-material-design-icons/ChevronLeft.vue";
|
||||
import ChevronRight from "vue-material-design-icons/ChevronRight.vue";
|
||||
|
||||
const props = defineProps<{ execution: Execution }>();
|
||||
|
||||
const currentPage = ref(1);
|
||||
|
||||
const total = ref(0);
|
||||
const results = ref<Execution[]>([]);
|
||||
|
||||
const currentIdx = ref(-1);
|
||||
|
||||
const prevDisabled = computed(
|
||||
() => total.value && currentIdx.value + 1 === total.value,
|
||||
);
|
||||
const nextDisabled = computed(() => total.value && currentIdx.value === 0);
|
||||
|
||||
const loadExecutions = async () => {
|
||||
const params = {
|
||||
"filters[namespace][PREFIX]": props.execution.namespace,
|
||||
"filters[flowId][EQUALS]": props.execution.flowId,
|
||||
"filters[timeRange][EQUALS]": "P365D", // Extended to 365 days for better navigation
|
||||
page: currentPage.value,
|
||||
size: 100,
|
||||
sort: "state.startDate:desc",
|
||||
};
|
||||
|
||||
const response = await store.findExecutions(params);
|
||||
|
||||
total.value = response.total;
|
||||
results.value.push(...response.results);
|
||||
|
||||
currentIdx.value = results.value.findIndex(
|
||||
(e: Execution) => e.id === props.execution.id,
|
||||
);
|
||||
|
||||
// If not found and more pages exist, load next page
|
||||
if (currentIdx.value === -1 && results.value.length < total.value) {
|
||||
currentPage.value += 1;
|
||||
await loadExecutions();
|
||||
}
|
||||
|
||||
// If found, move router
|
||||
if (currentIdx.value !== -1) {
|
||||
router.push(createLink("executions", results.value[currentIdx.value]));
|
||||
}
|
||||
};
|
||||
|
||||
const navigate = async (direction: "previous" | "next") => {
|
||||
if (currentIdx.value === -1) return;
|
||||
|
||||
if (direction === "previous") {
|
||||
if (prevDisabled.value) return;
|
||||
currentIdx.value += 1;
|
||||
} else {
|
||||
if (nextDisabled.value) return;
|
||||
currentIdx.value -= 1;
|
||||
}
|
||||
|
||||
// If we reached the end of loaded data but not total, load new page
|
||||
if (
|
||||
currentIdx.value >= results.value.length - 1 &&
|
||||
results.value.length < total.value
|
||||
) {
|
||||
currentPage.value += 1;
|
||||
await loadExecutions();
|
||||
} else {
|
||||
router.push(createLink("executions", results.value[currentIdx.value]));
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadExecutions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
#buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacer;
|
||||
|
||||
.el-button {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {Component} from "vue";
|
||||
|
||||
import {RouteLocationRaw} from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
41
ui/src/components/executions/overview/utils/links.ts
Normal file
41
ui/src/components/executions/overview/utils/links.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {RouteLocationRaw} from "vue-router";
|
||||
import {Execution} from "../../../../stores/executions";
|
||||
|
||||
type Types = "namespaces" | "flows" | "executions";
|
||||
|
||||
/**
|
||||
* Generates a Vue Router link object for a given execution and type.
|
||||
*
|
||||
* @param type - The type of route ("namespaces", "flows", or "executions").
|
||||
* @param execution - The execution object containing tenantId, namespace, flowId, and id.
|
||||
* @param customID - Optional ID to use instead of execution.id (only applies to "executions").
|
||||
* @returns A RouteLocationRaw object to be used with router navigation.
|
||||
*/
|
||||
export const createLink = (
|
||||
type: Types,
|
||||
execution: Execution,
|
||||
customID?: string,
|
||||
): RouteLocationRaw => {
|
||||
if (!execution) return {};
|
||||
|
||||
const params: Record<string, string> = {tab: "overview"};
|
||||
|
||||
if (execution?.tenantId) params.tenant = execution.tenantId;
|
||||
|
||||
switch (type) {
|
||||
case "namespaces":
|
||||
params.id = execution.namespace;
|
||||
break;
|
||||
case "flows":
|
||||
params.id = execution.flowId;
|
||||
params.namespace = execution.namespace;
|
||||
break;
|
||||
case "executions":
|
||||
params.id = customID ?? execution.id; // Use customID if provided, otherwise fallback to execution.id
|
||||
params.namespace = execution.namespace;
|
||||
params.flowId = execution.flowId;
|
||||
break;
|
||||
}
|
||||
|
||||
return {name: `${type}/update`, params};
|
||||
};
|
||||
@@ -26,7 +26,7 @@ export type Histories = {
|
||||
export interface Execution{
|
||||
id: string;
|
||||
namespace: string;
|
||||
flowId?: string;
|
||||
flowId: string;
|
||||
tenantId?: string;
|
||||
taskRunList: {
|
||||
id: string,
|
||||
@@ -80,7 +80,6 @@ export const useExecutionsStore = defineStore("executions", () => {
|
||||
const namespaces = ref<string[]>([]);
|
||||
const flowsExecutable = ref<any[]>([]);
|
||||
|
||||
|
||||
// clear flow graph when execution is reset
|
||||
// since it is supposed to represent the current execution's flow
|
||||
watch(execution, (newExecution) => {
|
||||
|
||||
Reference in New Issue
Block a user