mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
feat(ui); foreachItem progress bar
This commit is contained in:
@@ -166,10 +166,9 @@ public class TaskRun implements TenantInterface {
|
||||
}
|
||||
|
||||
public boolean isSame(TaskRun taskRun) {
|
||||
return this.getId().equals(taskRun.getId()) && (
|
||||
(this.getValue() == null && taskRun.getValue() == null) ||
|
||||
(this.getValue() != null && this.getValue().equals(taskRun.getValue()))
|
||||
);
|
||||
return this.getId().equals(taskRun.getId()) &&
|
||||
((this.getValue() == null && taskRun.getValue() == null) || (this.getValue() != null && this.getValue().equals(taskRun.getValue()))) &&
|
||||
((this.getItems() == null && taskRun.getItems() == null) || (this.getItems() != null && this.getItems().equals(taskRun.getItems()))) ;
|
||||
}
|
||||
|
||||
public String toString(boolean pretty) {
|
||||
|
||||
@@ -9,11 +9,11 @@ import java.util.List;
|
||||
|
||||
@Getter
|
||||
public class SubflowGraphTask extends AbstractGraphTask {
|
||||
public SubflowGraphTask(ExecutableTask task, TaskRun taskRun, List<String> values, RelationType relationType) {
|
||||
public SubflowGraphTask(ExecutableTask<?> task, TaskRun taskRun, List<String> values, RelationType relationType) {
|
||||
super((Task) task, taskRun, values, relationType);
|
||||
}
|
||||
|
||||
public ExecutableTask getExecutableTask() {
|
||||
return (ExecutableTask) super.getTask();
|
||||
public ExecutableTask<?> getExecutableTask() {
|
||||
return (ExecutableTask<?>) super.getTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Optional;
|
||||
/**
|
||||
* Interface for tasks that generates subflow execution(s). Those tasks are handled in the Executor.
|
||||
*/
|
||||
public interface ExecutableTask {
|
||||
public interface ExecutableTask<T extends Output>{
|
||||
/**
|
||||
* Creates a list of WorkerTaskExecution for this task definition.
|
||||
* Each WorkerTaskExecution will generate a subflow execution.
|
||||
|
||||
@@ -43,7 +43,7 @@ public final class ExecutableUtils {
|
||||
.build();
|
||||
}
|
||||
|
||||
public static <T extends Task & ExecutableTask> WorkerTaskExecution<?> workerTaskExecution(
|
||||
public static <T extends Task & ExecutableTask<?>> WorkerTaskExecution<?> workerTaskExecution(
|
||||
RunContext runContext,
|
||||
FlowExecutorInterface flowExecutorInterface,
|
||||
Execution currentExecution,
|
||||
@@ -51,7 +51,8 @@ public final class ExecutableUtils {
|
||||
T currentTask,
|
||||
TaskRun currentTaskRun,
|
||||
Map<String, Object> inputs,
|
||||
List<Label> labels
|
||||
List<Label> labels,
|
||||
Integer iteration
|
||||
) throws IllegalVariableEvaluationException {
|
||||
String subflowNamespace = runContext.render(currentTask.subflowId().namespace());
|
||||
String subflowId = runContext.render(currentTask.subflowId().flowId());
|
||||
@@ -100,6 +101,7 @@ public final class ExecutableUtils {
|
||||
.task(currentTask)
|
||||
.taskRun(currentTaskRun)
|
||||
.execution(execution)
|
||||
.iteration(iteration)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ public final class ExecutableUtils {
|
||||
|
||||
int currentStateIteration = getIterationCounter(iterations, currentState, maxIterations) + 1;
|
||||
iterations.put(currentState.toString(), currentStateIteration);
|
||||
if(previousState.isPresent() && previousState.get() != currentState) {
|
||||
if (previousState.isPresent() && previousState.get() != currentState) {
|
||||
int previousStateIterations = getIterationCounter(iterations, previousState.get(), maxIterations) - 1;
|
||||
iterations.put(previousState.get().toString(), previousStateIterations);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class Executor {
|
||||
private final List<WorkerTaskResult> workerTaskResults = new ArrayList<>();
|
||||
private final List<ExecutionDelay> executionDelays = new ArrayList<>();
|
||||
private WorkerTaskResult joined;
|
||||
private final List<WorkerTaskExecution> workerTaskExecutions = new ArrayList<>();
|
||||
private final List<WorkerTaskExecution<?>> workerTaskExecutions = new ArrayList<>();
|
||||
private ExecutionsRunning executionsRunning;
|
||||
private ExecutionQueued executionQueued;
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ public class ExecutorService {
|
||||
return false;
|
||||
}
|
||||
|
||||
var executableTask = (Task & ExecutableTask) workerTask.getTask();
|
||||
var executableTask = (Task & ExecutableTask<?>) workerTask.getTask();
|
||||
try {
|
||||
// mark taskrun as running to avoid multiple try for failed
|
||||
TaskRun executableTaskRun = executor.getExecution()
|
||||
|
||||
@@ -11,7 +11,7 @@ import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class WorkerTaskExecution<T extends Task & ExecutableTask> {
|
||||
public class WorkerTaskExecution<T extends Task & ExecutableTask<?>> {
|
||||
@NotNull
|
||||
private TaskRun taskRun;
|
||||
|
||||
@@ -20,4 +20,6 @@ public class WorkerTaskExecution<T extends Task & ExecutableTask> {
|
||||
|
||||
@NotNull
|
||||
private Execution execution;
|
||||
|
||||
private Integer iteration;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import java.util.*;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class Flow extends Task implements ExecutableTask {
|
||||
public class Flow extends Task implements ExecutableTask<Flow.Output> {
|
||||
@NotNull
|
||||
@Schema(
|
||||
title = "The namespace of the subflow to be executed"
|
||||
@@ -137,7 +137,8 @@ public class Flow extends Task implements ExecutableTask {
|
||||
this,
|
||||
currentTaskRun,
|
||||
inputs,
|
||||
labels
|
||||
labels,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.executions.TaskRun;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.tasks.ExecutableTask;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.runners.ExecutableUtils;
|
||||
@@ -70,7 +69,7 @@ import static io.kestra.core.utils.Rethrow.throwFunction;
|
||||
)
|
||||
}
|
||||
)
|
||||
public class ForEachItem extends Task implements ExecutableTask {
|
||||
public class ForEachItem extends Task implements ExecutableTask<ForEachItem.Output> {
|
||||
@NotEmpty
|
||||
@PluginProperty(dynamic = true)
|
||||
@Schema(title = "The items to be split into batches and processed. Make sure to set it to Kestra's internal storage URI, e.g. output from a previous task in the format `{{ outputs.task_id.uri }}` or an input parameter of FILE type e.g. `{{ inputs.myfile }}`.")
|
||||
@@ -171,6 +170,7 @@ public class ForEachItem extends Task implements ExecutableTask {
|
||||
}
|
||||
|
||||
int iteration = currentIteration.getAndIncrement();
|
||||
var outputs = Output.builder().iterations(Map.of("max", splits.size())).build();
|
||||
return ExecutableUtils.workerTaskExecution(
|
||||
runContext,
|
||||
flowExecutorInterface,
|
||||
@@ -178,13 +178,11 @@ public class ForEachItem extends Task implements ExecutableTask {
|
||||
currentFlow,
|
||||
this,
|
||||
currentTaskRun
|
||||
.withValue(String.valueOf(iteration))
|
||||
.withOutputs(Map.of(
|
||||
"iterations", Map.of("max", splits.size())
|
||||
))
|
||||
.withOutputs(outputs.toMap())
|
||||
.withItems(split.toString()),
|
||||
inputs,
|
||||
labels
|
||||
labels,
|
||||
iteration
|
||||
);
|
||||
}
|
||||
))
|
||||
@@ -231,4 +229,14 @@ public class ForEachItem extends Task implements ExecutableTask {
|
||||
@Builder.Default
|
||||
private String separator = "\n";
|
||||
}
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public static class Output implements io.kestra.core.models.tasks.Output {
|
||||
@Schema(
|
||||
title = "The iterations counter.",
|
||||
description = "This output will be updated in real-time with the subflow executions.\n It will contains one counter by subflow execution state, plus a `max` counter that represent the maximum number of iterations (or the number of batches)."
|
||||
)
|
||||
private final Map<String, Integer> iterations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ public class FlowTopologyService {
|
||||
.allTasksWithChilds()
|
||||
.stream()
|
||||
.filter(t -> t instanceof ExecutableTask)
|
||||
.map(t -> (ExecutableTask) t)
|
||||
.map(t -> (ExecutableTask<?>) t)
|
||||
.anyMatch(t ->
|
||||
t.subflowId().namespace().equals(child.getNamespace()) && t.subflowId().flowId().equals(child.getId())
|
||||
);
|
||||
|
||||
@@ -271,7 +271,7 @@ public class GraphUtils {
|
||||
// detect kids
|
||||
if (currentTask instanceof FlowableTask<?> flowableTask) {
|
||||
currentGraph = flowableTask.tasksTree(execution, currentTaskRun, parentValues);
|
||||
} else if (currentTask instanceof ExecutableTask subflowTask) {
|
||||
} else if (currentTask instanceof ExecutableTask<?> subflowTask) {
|
||||
currentGraph = new SubflowGraphTask(subflowTask, currentTaskRun, parentValues, relationType);
|
||||
} else {
|
||||
currentGraph = new GraphTask(currentTask, currentTaskRun, parentValues, relationType);
|
||||
|
||||
@@ -217,7 +217,7 @@ public class ValidationFactory {
|
||||
.stream()
|
||||
.forEach(
|
||||
task -> {
|
||||
if (task instanceof ExecutableTask executableTask
|
||||
if (task instanceof ExecutableTask<?> executableTask
|
||||
&& value.getId().equals(executableTask.subflowId().flowId())
|
||||
&& value.getNamespace().equals(executableTask.subflowId().namespace())) {
|
||||
violations.add("Recursive call to flow [" + value.getNamespace() + "." + value.getId() + "]");
|
||||
|
||||
@@ -13,13 +13,13 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AbstractJdbcWorkerTaskExecutionStorage extends AbstractJdbcRepository {
|
||||
protected io.kestra.jdbc.AbstractJdbcRepository<WorkerTaskExecution> jdbcRepository;
|
||||
protected io.kestra.jdbc.AbstractJdbcRepository<WorkerTaskExecution<?>> jdbcRepository;
|
||||
|
||||
public AbstractJdbcWorkerTaskExecutionStorage(io.kestra.jdbc.AbstractJdbcRepository<WorkerTaskExecution> jdbcRepository) {
|
||||
public AbstractJdbcWorkerTaskExecutionStorage(io.kestra.jdbc.AbstractJdbcRepository jdbcRepository) {
|
||||
this.jdbcRepository = jdbcRepository;
|
||||
}
|
||||
|
||||
public Optional<WorkerTaskExecution> get(String executionId) {
|
||||
public Optional<WorkerTaskExecution<?>> get(String executionId) {
|
||||
return this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transactionResult(configuration -> {
|
||||
@@ -35,12 +35,13 @@ public abstract class AbstractJdbcWorkerTaskExecutionStorage extends AbstractJdb
|
||||
});
|
||||
}
|
||||
|
||||
public void save(List<WorkerTaskExecution> workerTaskExecutions) {
|
||||
public void save(List<WorkerTaskExecution<?>> workerTaskExecutions) {
|
||||
this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transaction(configuration -> {
|
||||
DSLContext context = DSL.using(configuration);
|
||||
|
||||
// TODO batch insert
|
||||
workerTaskExecutions.forEach(workerTaskExecution -> {
|
||||
Map<Field<Object>, Object> fields = this.jdbcRepository.persistFields(workerTaskExecution);
|
||||
this.jdbcRepository.persist(workerTaskExecution, context, fields);
|
||||
@@ -48,7 +49,7 @@ public abstract class AbstractJdbcWorkerTaskExecutionStorage extends AbstractJdb
|
||||
});
|
||||
}
|
||||
|
||||
public void delete(WorkerTaskExecution workerTaskExecution) {
|
||||
public void delete(WorkerTaskExecution<?> workerTaskExecution) {
|
||||
this.jdbcRepository.delete(workerTaskExecution);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,10 +379,10 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
if (!executor.getWorkerTaskExecutions().isEmpty()) {
|
||||
workerTaskExecutionStorage.save(executor.getWorkerTaskExecutions());
|
||||
|
||||
List<WorkerTaskExecution> workerTasksExecutionDedup = executor
|
||||
List<WorkerTaskExecution<?>> workerTasksExecutionDedup = executor
|
||||
.getWorkerTaskExecutions()
|
||||
.stream()
|
||||
.filter(workerTaskExecution -> this.deduplicateWorkerTaskExecution(execution, executorState, workerTaskExecution.getTaskRun()))
|
||||
.filter(workerTaskExecution -> this.deduplicateWorkerTaskExecution(execution, executorState, workerTaskExecution.getTaskRun(), workerTaskExecution.getIteration()))
|
||||
.toList();
|
||||
|
||||
workerTasksExecutionDedup
|
||||
@@ -405,7 +405,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
executionQueue.emit(workerTaskExecution.getExecution());
|
||||
|
||||
// send a running worker task result to track running vs created status
|
||||
if (((ExecutableTask) workerTaskExecution.getTask()).waitForExecution()) {
|
||||
if (workerTaskExecution.getTask().waitForExecution()) {
|
||||
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING));
|
||||
}
|
||||
});
|
||||
@@ -425,7 +425,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
workerTaskExecutionStorage.get(execution.getId())
|
||||
.ifPresent(workerTaskExecution -> {
|
||||
// If we didn't wait for the flow execution, the worker task execution has already been created by the Executor service.
|
||||
if (((ExecutableTask) workerTaskExecution.getTask()).waitForExecution()) {
|
||||
if (workerTaskExecution.getTask().waitForExecution()) {
|
||||
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING).withState(execution.getState().getCurrent()));
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
private void sendWorkerTaskResultForWorkerTaskExecution(Execution execution, WorkerTaskExecution<?> workerTaskExecution, TaskRun taskRun) {
|
||||
Flow workerTaskFlow = this.flowRepository.findByExecution(execution);
|
||||
|
||||
ExecutableTask executableTask = workerTaskExecution.getTask();
|
||||
ExecutableTask<?> executableTask = workerTaskExecution.getTask();
|
||||
|
||||
RunContext runContext = runContextFactory.of(
|
||||
workerTaskFlow,
|
||||
@@ -693,9 +693,9 @@ public class JdbcExecutor implements ExecutorInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deduplicateWorkerTaskExecution(Execution execution, ExecutorState executorState, TaskRun taskRun) {
|
||||
// There can be multiple executions for the same task, so we need to deduplicated with the taskrun.value
|
||||
String deduplicationKey = taskRun.getId() + "-" + taskRun.getValue();
|
||||
private boolean deduplicateWorkerTaskExecution(Execution execution, ExecutorState executorState, TaskRun taskRun, Integer iteration) {
|
||||
// There can be multiple executions for the same task, so we need to deduplicated with the worker task execution iteration
|
||||
String deduplicationKey = taskRun.getId() + (iteration == null ? "" : "-" + iteration);
|
||||
State.Type current = executorState.getWorkerTaskExecutionDeduplication().get(deduplicationKey);
|
||||
|
||||
if (current == taskRun.getState().getCurrent()) {
|
||||
|
||||
@@ -28,7 +28,7 @@ public abstract class AbstractWorkerTaskExecutionTest {
|
||||
@Test
|
||||
void suite() throws Exception {
|
||||
|
||||
WorkerTaskExecution workerTaskExecution = WorkerTaskExecution.builder()
|
||||
WorkerTaskExecution<?> workerTaskExecution = WorkerTaskExecution.builder()
|
||||
.execution(Execution.builder().id(IdUtils.create()).build())
|
||||
.task(Flow.builder().type(Flow.class.getName()).id(IdUtils.create()).build())
|
||||
.taskRun(TaskRun.builder().id(IdUtils.create()).build())
|
||||
@@ -37,7 +37,7 @@ public abstract class AbstractWorkerTaskExecutionTest {
|
||||
workerTaskExecutionStorage.save(List.of(workerTaskExecution));
|
||||
|
||||
|
||||
Optional<WorkerTaskExecution> find = workerTaskExecutionStorage.get(workerTaskExecution.getExecution().getId());
|
||||
Optional<WorkerTaskExecution<?>> find = workerTaskExecutionStorage.get(workerTaskExecution.getExecution().getId());
|
||||
assertThat(find.isPresent(), is(true));
|
||||
assertThat(find.get().getExecution().getId(), is(workerTaskExecution.getExecution().getId()));
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ public class MemoryExecutor implements ExecutorInterface {
|
||||
executionQueue.emit(workerTaskExecution.getExecution());
|
||||
|
||||
// send a running worker task result to track running vs created status
|
||||
if (((ExecutableTask) workerTaskExecution.getTask()).waitForExecution()) {
|
||||
if (workerTaskExecution.getTask().waitForExecution()) {
|
||||
sendWorkerTaskResultForWorkerTaskExecution(execution, workerTaskExecution, workerTaskExecution.getTaskRun().withState(State.Type.RUNNING));
|
||||
}
|
||||
});
|
||||
@@ -271,7 +271,7 @@ public class MemoryExecutor implements ExecutorInterface {
|
||||
try {
|
||||
Flow workerTaskFlow = this.flowRepository.findByExecution(execution);
|
||||
|
||||
ExecutableTask executableTask = workerTaskExecution.getTask();
|
||||
ExecutableTask<?> executableTask = workerTaskExecution.getTask();
|
||||
|
||||
RunContext runContext = runContextFactory.of(
|
||||
workerTaskFlow,
|
||||
|
||||
143
ui/src/components/executions/ForEachStatus.vue
Normal file
143
ui/src/components/executions/ForEachStatus.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="m-3">
|
||||
<div class="progress">
|
||||
<div
|
||||
v-for="state in State.allStates()"
|
||||
:key="state.key"
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:class="`bg-${state.colorClass}`"
|
||||
:style="`width: ${getPercentage(state.key)}%`"
|
||||
:aria-valuenow="getPercentage(state.key)"
|
||||
aria-valuemin="0"
|
||||
:aria-valuemax="localSubflowStatus.max"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 d-flex">
|
||||
<el-button @click="goToExecutionsList(null)" class="count-button">
|
||||
{{ $t("all executions") }} <span class="counter">{{ localSubflowStatus.max }}</span>
|
||||
</el-button>
|
||||
<div v-for="state in State.allStates()" :key="state.key">
|
||||
<el-button @click="goToExecutionsList(state.key)" class="count-button" v-if="localSubflowStatus[state.key] >= 0">
|
||||
<div class="dot rounded-5" :class="`bg-${state.colorClass}`"></div>
|
||||
{{ capitalizeFirstLetter(state.key) }}
|
||||
<span class="counter">{{ localSubflowStatus[state.key] }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {cssVariable} from "../../utils/global"
|
||||
import State from "../../utils/state";
|
||||
import throttle from "lodash/throttle"
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
State() {
|
||||
return State
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localSubflowStatus: {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.localSubflowStatus = this.subflowsStatus
|
||||
},
|
||||
props: {
|
||||
subflowsStatus: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
taskRunId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
subflowsStatus() {
|
||||
this.updateThrottled();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cssVariable,
|
||||
getPercentage(state) {
|
||||
if (!this.localSubflowStatus[state]) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round((this.localSubflowStatus[state] / this.localSubflowStatus.max) * 100);
|
||||
},
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
},
|
||||
goToExecutionsList(state) {
|
||||
this.$router.push({
|
||||
name: "executions/list",
|
||||
query: {
|
||||
parentId: this.taskRunId,
|
||||
state: state
|
||||
}
|
||||
});
|
||||
},
|
||||
updateThrottled: throttle(function () {
|
||||
this.localSubflowStatus = this.subflowsStatus
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.dot {
|
||||
width: 6.413px;
|
||||
height: 6.413px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
padding: 0.5rem 1rem;
|
||||
&:hover {
|
||||
html.dark & {
|
||||
border-color: #404559;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
html.dark & {
|
||||
border-color: #404559;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count-button {
|
||||
color: var(--text-color);
|
||||
padding: 4px 8px;
|
||||
margin-right: 0.5rem;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
html.dark & {
|
||||
background: #404559;
|
||||
}
|
||||
font-size: 0.75rem;
|
||||
|
||||
}
|
||||
|
||||
.counter {
|
||||
padding: 0 4px;
|
||||
margin-left: 0.5rem;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--bs-gray-300);
|
||||
html.dark & {
|
||||
background: #21242E;
|
||||
}
|
||||
font-size: 0.65rem;
|
||||
line-height: 1.0625rem;
|
||||
}
|
||||
</style>
|
||||
@@ -2,71 +2,72 @@
|
||||
<el-card shadow="never" v-if="execution">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<duration :histories="execution.state.histories" />
|
||||
</th>
|
||||
<td v-for="(date, i) in dates" :key="i">
|
||||
{{ date }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<duration :histories="execution.state.histories" />
|
||||
</th>
|
||||
<td v-for="(date, i) in dates" :key="i">
|
||||
{{ date }}
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="currentTaskRun in partialSeries" :key="currentTaskRun.id">
|
||||
<tr>
|
||||
<th>
|
||||
<el-tooltip placement="top-start" :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
<code>{{ currentTaskRun.name }}</code>
|
||||
<small v-if="currentTaskRun.task && currentTaskRun.task.value"><br>{{ currentTaskRun.task.value }}</small>
|
||||
</template>
|
||||
<span>
|
||||
<code>{{ currentTaskRun.name }}</code>
|
||||
<small v-if="currentTaskRun.task && currentTaskRun.task.value"> {{ currentTaskRun.task.value }}</small>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<td :colspan="dates.length">
|
||||
<el-tooltip placement="top" :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
<span style="white-space: pre-wrap;">
|
||||
{{ currentTaskRun.tooltip }}
|
||||
<tr>
|
||||
<th>
|
||||
<el-tooltip placement="top-start" :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
<code>{{ currentTaskRun.name }}</code>
|
||||
<small v-if="currentTaskRun.task && currentTaskRun.task.value"><br>{{ currentTaskRun.task.value }}</small>
|
||||
</template>
|
||||
<span>
|
||||
<code>{{ currentTaskRun.name }}</code>
|
||||
<small v-if="currentTaskRun.task && currentTaskRun.task.value"> {{ currentTaskRun.task.value }}</small>
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
:style="{left: currentTaskRun.start + '%', width: currentTaskRun.width + '%'}"
|
||||
class="task-progress"
|
||||
@click="onTaskSelect(currentTaskRun.task)"
|
||||
>
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{left: currentTaskRun.left + '%', width: (100-currentTaskRun.left) + '%'}"
|
||||
:class="'bg-' + currentTaskRun.color + (currentTaskRun.running ? ' progress-bar-striped progress-bar-animated' : '')"
|
||||
role="progressbar"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</th>
|
||||
<td :colspan="dates.length">
|
||||
<el-tooltip placement="top" :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
<span style="white-space: pre-wrap;">
|
||||
{{ currentTaskRun.tooltip }}
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
:style="{left: currentTaskRun.start + '%', width: currentTaskRun.width + '%'}"
|
||||
class="task-progress"
|
||||
@click="onTaskSelect(currentTaskRun.task)"
|
||||
>
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:style="{left: currentTaskRun.left + '%', width: (100-currentTaskRun.left) + '%'}"
|
||||
:class="'bg-' + currentTaskRun.color + (currentTaskRun.running ? ' progress-bar-striped progress-bar-animated' : '')"
|
||||
role="progressbar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="selectedTaskRun?.id === currentTaskRun.id">
|
||||
<td :colspan="dates.length + 1" class="p-0 pb-2">
|
||||
<log-list
|
||||
:task-run-id="selectedTaskRun.id"
|
||||
:exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
|
||||
level="TRACE"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
:target-execution="execution"
|
||||
:target-flow="flow"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</el-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="selectedTaskRun?.id === currentTaskRun.id">
|
||||
<td :colspan="dates.length + 1" class="p-0 pb-2">
|
||||
<task-run-details
|
||||
:task-run-id="selectedTaskRun.id"
|
||||
:exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
|
||||
level="TRACE"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
:target-execution="execution"
|
||||
:target-flow="flow"
|
||||
:show-logs="!currentTaskRun?.task?.outputs?.iterations"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</el-card>
|
||||
</template>
|
||||
<script>
|
||||
import LogList from "../logs/LogList.vue";
|
||||
import TaskRunDetails from "../logs/TaskRunDetails.vue";
|
||||
import {mapState} from "vuex";
|
||||
import State from "../../utils/state";
|
||||
import Duration from "../layout/Duration.vue";
|
||||
@@ -75,7 +76,7 @@
|
||||
const ts = date => new Date(date).getTime();
|
||||
const TASKRUN_THRESHOLD = 50
|
||||
export default {
|
||||
components: {LogList, Duration},
|
||||
components: {TaskRunDetails, Duration},
|
||||
data() {
|
||||
return {
|
||||
colors: State.colorClass(),
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</el-form-item>
|
||||
</collapse>
|
||||
|
||||
<log-list
|
||||
<task-run-details
|
||||
ref="logs"
|
||||
:level="level"
|
||||
:exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LogList from "../logs/LogList.vue";
|
||||
import TaskRunDetails from "../logs/TaskRunDetails.vue";
|
||||
import {mapState} from "vuex";
|
||||
import Download from "vue-material-design-icons/Download.vue";
|
||||
import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LogList,
|
||||
TaskRunDetails,
|
||||
LogLevelSelector,
|
||||
Kicon,
|
||||
Download,
|
||||
|
||||
365
ui/src/components/executions/TaskRunLine.vue
Normal file
365
ui/src/components/executions/TaskRunLine.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="attempt-header">
|
||||
<div class="task-icon d-none d-md-inline-block me-1">
|
||||
<task-icon
|
||||
:cls="taskType(currentTaskRun)"
|
||||
v-if="taskType(currentTaskRun)"
|
||||
only-icon
|
||||
:icons="icons"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="task-id flex-grow-1"
|
||||
:id="`attempt-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${currentTaskRun.id}`"
|
||||
>
|
||||
<el-tooltip :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
{{ $t("from") }} :
|
||||
{{ $filters.date(selectedAttempt(currentTaskRun).state.startDate) }}
|
||||
<br>
|
||||
{{ $t("to") }} :
|
||||
{{ $filters.date(selectedAttempt(currentTaskRun).state.endDate) }}
|
||||
<br>
|
||||
<clock/>
|
||||
<strong>{{ $t("duration") }}:</strong>
|
||||
{{ $filters.humanizeDuration(selectedAttempt(currentTaskRun).state.duration) }}
|
||||
</template>
|
||||
<span>
|
||||
<span class="me-1 fw-bold">{{ currentTaskRun.taskId }}</span>
|
||||
<small v-if="currentTaskRun.value">
|
||||
{{ currentTaskRun.value }}
|
||||
</small>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="task-duration d-none d-md-inline-block">
|
||||
<small class="me-1">
|
||||
<duration :histories="selectedAttempt(currentTaskRun).state.histories"/>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="task-status">
|
||||
<status size="small" :status="selectedAttempt(currentTaskRun).state.current"/>
|
||||
</div>
|
||||
|
||||
<el-select
|
||||
class="d-none d-md-inline-block"
|
||||
:model-value="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@change="forwardEvent('swapDisplayedAttempt',(currentTaskRun.id, $event))"
|
||||
:disabled="!currentTaskRun.attempts || currentTaskRun.attempts?.length <= 1"
|
||||
>
|
||||
<el-option
|
||||
v-for="(_, index) in attempts(currentTaskRun)"
|
||||
:key="`attempt-${index}-${currentTaskRun.id}`"
|
||||
:value="index"
|
||||
:label="`${$t('attempt')} ${index + 1}`"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="default" class="more-dropdown-button">
|
||||
<DotsHorizontal title=""/>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<sub-flow-link
|
||||
v-if="isSubflow(currentTaskRun)"
|
||||
component="el-dropdown-item"
|
||||
tab-execution="logs"
|
||||
:execution-id="currentTaskRun.outputs.executionId"
|
||||
/>
|
||||
|
||||
<metrics :task-run="currentTaskRun" :execution="followedExecution"/>
|
||||
|
||||
<outputs
|
||||
:outputs="currentTaskRun.outputs"
|
||||
:execution="followedExecution"
|
||||
/>
|
||||
|
||||
<restart
|
||||
component="el-dropdown-item"
|
||||
:key="`restart-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${selectedAttempt(currentTaskRun).state.startDate}`"
|
||||
is-replay
|
||||
tooltip-position="left"
|
||||
:execution="followedExecution"
|
||||
:task-run="currentTaskRun"
|
||||
:attempt-index="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
/>
|
||||
|
||||
<change-status
|
||||
component="el-dropdown-item"
|
||||
:key="`change-status-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${selectedAttempt(currentTaskRun).state.startDate}`"
|
||||
:execution="followedExecution"
|
||||
:task-run="currentTaskRun"
|
||||
:attempt-index="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
/>
|
||||
<task-edit
|
||||
:read-only="true"
|
||||
component="el-dropdown-item"
|
||||
:task-id="currentTaskRun.taskId"
|
||||
:section="SECTIONS.TASKS"
|
||||
:flow-id="followedExecution.flowId"
|
||||
:namespace="followedExecution.namespace"
|
||||
:revision="followedExecution.flowRevision"
|
||||
:flow-source="flow?.source"
|
||||
/>
|
||||
<el-dropdown-item
|
||||
:icon="Download"
|
||||
@click="downloadContent(currentTaskRun.id)"
|
||||
>
|
||||
{{ $t("download logs") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<el-button
|
||||
v-if="!taskRunId && shouldDisplayChevron(currentTaskRun)"
|
||||
class="border-0 expand-collapse"
|
||||
type="default"
|
||||
text
|
||||
@click.stop="() => forwardEvent('toggleShowAttempt',(attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id])))"
|
||||
>
|
||||
<ChevronDown
|
||||
v-if="shownAttemptsUid.includes(attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id]))"
|
||||
/>
|
||||
<ChevronUp v-else/>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Restart from "./Restart.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
import Metrics from "./Metrics.vue";
|
||||
import Status from "../Status.vue";
|
||||
import ChangeStatus from "./ChangeStatus.vue";
|
||||
import TaskEdit from "../flows/TaskEdit.vue";
|
||||
import SubFlowLink from "../flows/SubFlowLink.vue";
|
||||
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
import Clock from "vue-material-design-icons/Clock.vue";
|
||||
import Outputs from "./Outputs.vue";
|
||||
import State from "../../utils/state";
|
||||
import FlowUtils from "../../utils/flowUtils";
|
||||
import {mapState} from "vuex";
|
||||
import {SECTIONS} from "../../utils/constants";
|
||||
import Download from "vue-material-design-icons/Download.vue";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
import Duration from "../layout/Duration.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TaskIcon,
|
||||
Outputs,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
DotsHorizontal,
|
||||
SubFlowLink,
|
||||
TaskEdit,
|
||||
ChangeStatus,
|
||||
Status,
|
||||
Metrics,
|
||||
ChevronUp,
|
||||
Restart,
|
||||
Duration
|
||||
},
|
||||
mounted() {
|
||||
if (this.targetExecutionId) {
|
||||
this.followExecution(this.targetExecutionId);
|
||||
}
|
||||
},
|
||||
props: {
|
||||
currentTaskRun: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
followedExecution: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
flow: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
forcedAttemptNumber: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
taskRunId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
selectedAttemptNumberByTaskRunId: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
shownAttemptsUid: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
logs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Download() {
|
||||
return Download
|
||||
},
|
||||
...mapState("plugin", ["icons"]),
|
||||
SECTIONS() {
|
||||
return SECTIONS
|
||||
},
|
||||
currentTaskRuns() {
|
||||
return this.followedExecution?.taskRunList?.filter(tr => this.taskRunId ? tr.id === this.taskRunId : true) ?? [];
|
||||
},
|
||||
taskRunById() {
|
||||
return Object.fromEntries(this.currentTaskRuns.map(taskRun => [taskRun.id, taskRun]));
|
||||
},
|
||||
logsWithIndexByAttemptUid() {
|
||||
const indexedLogs = this.logs
|
||||
.filter(logLine => logLine.message.toLowerCase().includes(this.filter) || this.isSubflow(this.taskRunById[logLine.taskRunId]))
|
||||
.map((logLine, index) => ({...logLine, index}));
|
||||
|
||||
return _groupBy(indexedLogs, indexedLog => this.attemptUid(indexedLog.taskRunId, indexedLog.attemptNumber));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
attempts(taskRun) {
|
||||
if (this.followedExecution.state.current === State.RUNNING || this.forcedAttemptNumber === undefined) {
|
||||
return taskRun.attempts ?? [{state: taskRun.state}];
|
||||
}
|
||||
|
||||
return taskRun.attempts ? [taskRun.attempts[this.forcedAttemptNumber]] : [];
|
||||
},
|
||||
isSubflow(taskRun) {
|
||||
return taskRun.outputs?.executionId;
|
||||
},
|
||||
selectedAttempt(taskRun) {
|
||||
return this.attempts(taskRun)[this.selectedAttemptNumberByTaskRunId[taskRun.id] ?? 0];
|
||||
},
|
||||
taskType(taskRun) {
|
||||
const task = FlowUtils.findTaskById(this.flow, taskRun.taskId);
|
||||
const parentTaskRunId = taskRun.parentTaskRunId;
|
||||
if (task === undefined && parentTaskRunId) {
|
||||
return this.taskType(this.taskRunById[parentTaskRunId])
|
||||
}
|
||||
return task ? task.type : undefined;
|
||||
},
|
||||
downloadContent(currentTaskRunId) {
|
||||
const params = this.params
|
||||
this.$store.dispatch("execution/downloadLogs", {
|
||||
executionId: this.followedExecution.id,
|
||||
params: {...params, taskRunId: currentTaskRunId}
|
||||
}).then((response) => {
|
||||
const url = window.URL.createObjectURL(new Blob([response]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", this.downloadName(currentTaskRunId));
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
});
|
||||
},
|
||||
forwardEvent(type, event) {
|
||||
this.$emit(type, event);
|
||||
},
|
||||
attemptUid(taskRunId, attemptNumber) {
|
||||
return `${taskRunId}-${attemptNumber}`
|
||||
},
|
||||
shouldDisplayChevron(taskRun) {
|
||||
return this.shouldDisplayProgressBar(taskRun) || this.shouldDisplayLogs(taskRun.id)
|
||||
},
|
||||
shouldDisplayProgressBar(taskRun) {
|
||||
return this.taskType(taskRun) === "io.kestra.core.tasks.flows.ForEachItem"
|
||||
},
|
||||
shouldDisplayLogs(taskRunId) {
|
||||
return this.logsWithIndexByAttemptUid[this.attemptUid(taskRunId, this.selectedAttemptNumberByTaskRunId[taskRunId])]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "@kestra-io/ui-libs/src/scss/variables";
|
||||
|
||||
.attempt-header {
|
||||
display: flex;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.attempt-number {
|
||||
background: var(--bs-gray-400);
|
||||
padding: .375rem .75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
html.dark & {
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.task-id, .task-duration {
|
||||
padding: .375rem 0;
|
||||
}
|
||||
|
||||
.task-id {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
span span {
|
||||
color: var(--bs-tertiary-color);
|
||||
|
||||
html:not(.dark) & {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 36px;
|
||||
padding: 6px;
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--bs-gray-600);
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: var(--font-size-xs)
|
||||
}
|
||||
|
||||
.task-duration small {
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--bs-gray-800);
|
||||
}
|
||||
|
||||
.more-dropdown-button {
|
||||
padding: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
border: 1px solid rgba($white, .05);
|
||||
|
||||
&:not(:hover) {
|
||||
background: rgba($white, .10);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
import TaskEdit from "../flows/TaskEdit.vue";
|
||||
import SearchField from "../layout/SearchField.vue";
|
||||
import LogLevelSelector from "../logs/LogLevelSelector.vue";
|
||||
import LogList from "../logs/LogList.vue";
|
||||
import TaskRunDetails from "../logs/TaskRunDetails.vue";
|
||||
import Collapse from "../layout/Collapse.vue";
|
||||
|
||||
// Topology
|
||||
@@ -366,7 +366,7 @@
|
||||
<log-level-selector :value="logLevel" @update:model-value="onLevelChange"/>
|
||||
</el-form-item>
|
||||
</collapse>
|
||||
<log-list
|
||||
<task-run-details
|
||||
v-for="taskRun in selectedTask.taskRuns"
|
||||
:key="taskRun.id"
|
||||
:target-execution-id="selectedTask.execution?.id"
|
||||
|
||||
@@ -5,133 +5,25 @@
|
||||
v-if="uniqueTaskRunDisplayFilter(currentTaskRun)"
|
||||
>
|
||||
<el-card class="attempt-wrapper">
|
||||
<div class="attempt-header">
|
||||
<div class="task-icon d-none d-md-inline-block me-1">
|
||||
<task-icon
|
||||
:cls="taskIcon(currentTaskRun)"
|
||||
v-if="taskIcon(currentTaskRun)"
|
||||
only-icon
|
||||
:icons="icons"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-id flex-grow-1"
|
||||
:id="`attempt-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${currentTaskRun.id}`"
|
||||
>
|
||||
<el-tooltip :persistent="false" transition="" :hide-after="0">
|
||||
<template #content>
|
||||
{{ $t("from") }} :
|
||||
{{ $filters.date(selectedAttempt(currentTaskRun).state.startDate) }}
|
||||
<br>
|
||||
{{ $t("to") }} :
|
||||
{{ $filters.date(selectedAttempt(currentTaskRun).state.endDate) }}
|
||||
<br>
|
||||
<clock />
|
||||
<strong>{{ $t("duration") }}:</strong>
|
||||
{{ $filters.humanizeDuration(selectedAttempt(currentTaskRun).state.duration) }}
|
||||
</template>
|
||||
<span>
|
||||
<span class="me-1 fw-bold">{{ currentTaskRun.taskId }}</span>
|
||||
<small v-if="currentTaskRun.value">
|
||||
{{ currentTaskRun.value }}
|
||||
</small>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="task-duration d-none d-md-inline-block">
|
||||
<small class="me-1">
|
||||
<duration :histories="selectedAttempt(currentTaskRun).state.histories" />
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="task-status">
|
||||
<status size="small" :status="selectedAttempt(currentTaskRun).state.current" />
|
||||
</div>
|
||||
|
||||
<el-select
|
||||
class="d-none d-md-inline-block"
|
||||
v-model="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@change="swapDisplayedAttempt(currentTaskRun.id, $event)"
|
||||
:disabled="!currentTaskRun.attempts || currentTaskRun.attempts?.length <= 1"
|
||||
>
|
||||
<el-option v-for="(_, index) in attempts(currentTaskRun)"
|
||||
:key="`attempt-${index}-${currentTaskRun.id}`"
|
||||
:value="index"
|
||||
:label="`${$t('attempt')} ${index + 1}`"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-dropdown trigger="click">
|
||||
<el-button type="default" class="more-dropdown-button">
|
||||
<DotsHorizontal title="" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<sub-flow-link
|
||||
v-if="isSubflow(currentTaskRun)"
|
||||
component="el-dropdown-item"
|
||||
tab-execution="logs"
|
||||
:execution-id="currentTaskRun.outputs.executionId"
|
||||
/>
|
||||
|
||||
<metrics :task-run="currentTaskRun" :execution="followedExecution" />
|
||||
|
||||
<outputs
|
||||
:outputs="currentTaskRun.outputs"
|
||||
:execution="followedExecution"
|
||||
/>
|
||||
|
||||
<restart
|
||||
component="el-dropdown-item"
|
||||
:key="`restart-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${selectedAttempt(currentTaskRun).state.startDate}`"
|
||||
is-replay
|
||||
tooltip-position="left"
|
||||
:execution="followedExecution"
|
||||
:task-run="currentTaskRun"
|
||||
:attempt-index="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
/>
|
||||
|
||||
<change-status
|
||||
component="el-dropdown-item"
|
||||
:key="`change-status-${selectedAttemptNumberByTaskRunId[currentTaskRun.id]}-${selectedAttempt(currentTaskRun).state.startDate}`"
|
||||
:execution="followedExecution"
|
||||
:task-run="currentTaskRun"
|
||||
:attempt-index="selectedAttemptNumberByTaskRunId[currentTaskRun.id]"
|
||||
@follow="forwardEvent('follow', $event)"
|
||||
/>
|
||||
<task-edit
|
||||
:read-only="true"
|
||||
component="el-dropdown-item"
|
||||
:task-id="currentTaskRun.taskId"
|
||||
:section="SECTIONS.TASKS"
|
||||
:flow-id="followedExecution.flowId"
|
||||
:namespace="followedExecution.namespace"
|
||||
:revision="followedExecution.flowRevision"
|
||||
:flow-source="flow?.source"
|
||||
/>
|
||||
<el-dropdown-item
|
||||
:icon="Download"
|
||||
@click="downloadContent(currentTaskRun.id)"
|
||||
>
|
||||
{{ $t("download logs") }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<el-button v-if="!taskRunId" class="border-0 expand-collapse" type="default" text
|
||||
@click.stop="() => toggleShowAttempt(attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id]))"
|
||||
>
|
||||
<ChevronDown
|
||||
v-if="!shownAttemptsUid.includes(attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id]))"
|
||||
/>
|
||||
<ChevronUp v-else />
|
||||
</el-button>
|
||||
|
||||
</div>
|
||||
<task-run-line
|
||||
:current-task-run="currentTaskRun"
|
||||
:followed-execution="followedExecution"
|
||||
:flow="flow"
|
||||
:forced-attempt-number="forcedAttemptNumber"
|
||||
:task-run-id="taskRunId"
|
||||
@toggle-show-attempt="toggleShowAttempt"
|
||||
@swap-displayed-attempt="swapDisplayedAttempt"
|
||||
:selected-attempt-number-by-task-run-id="selectedAttemptNumberByTaskRunId"
|
||||
:shown-attempts-uid="shownAttemptsUid"
|
||||
:logs="logs"
|
||||
/>
|
||||
<for-each-status
|
||||
v-if="shouldDisplayProgressBar(currentTaskRun) && showProgressBar"
|
||||
:task-run-id="currentTaskRun.id"
|
||||
:subflows-status="currentTaskRun.outputs.iterations"
|
||||
/>
|
||||
<DynamicScroller
|
||||
v-if="shouldDisplayLogs(currentTaskRun.id)"
|
||||
v-if="shouldDisplayLogs(currentTaskRun)"
|
||||
:items="logsWithIndexByAttemptUid[attemptUid(currentTaskRun.id, selectedAttemptNumberByTaskRunId[currentTaskRun.id])] ?? []"
|
||||
:min-item-size="50"
|
||||
key-field="index"
|
||||
@@ -152,14 +44,15 @@
|
||||
:exclude-metas="excludeMetas"
|
||||
v-if="filter === '' || item.message.toLowerCase().includes(filter)"
|
||||
/>
|
||||
<log-list v-if="!taskRunId && isSubflow(currentTaskRun) && currentTaskRun.outputs?.executionId"
|
||||
ref="subflows-logs"
|
||||
:level="level"
|
||||
:exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
|
||||
:filter="filter"
|
||||
:allow-auto-expand-subflows="false"
|
||||
:target-execution-id="currentTaskRun.outputs.executionId"
|
||||
:class="$el.classList.contains('even') ? '' : 'even'"
|
||||
<task-run-details
|
||||
v-if="!taskRunId && isSubflow(currentTaskRun) && currentTaskRun.outputs?.executionId"
|
||||
ref="subflows-logs"
|
||||
:level="level"
|
||||
:exclude-metas="['namespace', 'flowId', 'taskId', 'executionId']"
|
||||
:filter="filter"
|
||||
:allow-auto-expand-subflows="false"
|
||||
:target-execution-id="currentTaskRun.outputs.executionId"
|
||||
:class="$el.classList.contains('even') ? '' : 'even'"
|
||||
/>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
@@ -172,49 +65,27 @@
|
||||
|
||||
<script>
|
||||
import LogLine from "./LogLine.vue";
|
||||
import Restart from "../executions/Restart.vue";
|
||||
import ChangeStatus from "../executions/ChangeStatus.vue";
|
||||
import Metrics from "../executions/Metrics.vue";
|
||||
import Outputs from "../executions/Outputs.vue";
|
||||
import Clock from "vue-material-design-icons/Clock.vue";
|
||||
import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
|
||||
import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
|
||||
import State from "../../utils/state";
|
||||
import Status from "../Status.vue";
|
||||
import SubFlowLink from "../flows/SubFlowLink.vue"
|
||||
import TaskEdit from "../flows/TaskEdit.vue";
|
||||
import Duration from "../layout/Duration.vue";
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
import _xor from "lodash/xor";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import FlowUtils from "../../utils/flowUtils.js";
|
||||
import moment from "moment";
|
||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"
|
||||
import {logDisplayTypes, SECTIONS} from "../../utils/constants";
|
||||
import {logDisplayTypes} from "../../utils/constants";
|
||||
import Download from "vue-material-design-icons/Download.vue";
|
||||
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
||||
import {DynamicScroller, DynamicScrollerItem} from "vue-virtual-scroller";
|
||||
import {mapState} from "vuex";
|
||||
import ForEachStatus from "../executions/ForEachStatus.vue";
|
||||
import TaskRunLine from "../executions/TaskRunLine.vue";
|
||||
import FlowUtils from "../../utils/flowUtils";
|
||||
|
||||
export default {
|
||||
name: 'LogList',
|
||||
name: "TaskRunDetails",
|
||||
components: {
|
||||
TaskRunLine,
|
||||
ForEachStatus,
|
||||
LogLine,
|
||||
Restart,
|
||||
ChangeStatus,
|
||||
Clock,
|
||||
Metrics,
|
||||
Outputs,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Status,
|
||||
SubFlowLink,
|
||||
TaskEdit,
|
||||
Duration,
|
||||
TaskIcon,
|
||||
DynamicScroller,
|
||||
DynamicScrollerItem,
|
||||
DotsHorizontal
|
||||
},
|
||||
emits: ["opened-taskruns-count", "follow", "reset-expand-collapse-all-switch"],
|
||||
props: {
|
||||
@@ -256,6 +127,14 @@
|
||||
allowAutoExpandSubflows: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showProgressBar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showLogs: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -361,9 +240,6 @@
|
||||
},
|
||||
computed: {
|
||||
...mapState("plugin", ["icons"]),
|
||||
SECTIONS() {
|
||||
return SECTIONS
|
||||
},
|
||||
Download() {
|
||||
return Download
|
||||
},
|
||||
@@ -411,11 +287,11 @@
|
||||
this.shownAttemptsUid.length === 0 ? this.expandAll() : this.collapseAll();
|
||||
},
|
||||
autoExpandBasedOnSettings() {
|
||||
if(this.autoExpandTaskrunStates.length === 0) {
|
||||
if (this.autoExpandTaskrunStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.followedExecution === undefined) {
|
||||
if (this.followedExecution === undefined) {
|
||||
setTimeout(() => this.autoExpandBasedOnSettings(), 50);
|
||||
return;
|
||||
}
|
||||
@@ -429,19 +305,15 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
shouldDisplayLogs(taskRunId) {
|
||||
return this.taskRunId || (this.shownAttemptsUid.includes(this.attemptUid(taskRunId, this.selectedAttemptNumberByTaskRunId[taskRunId])) &&
|
||||
this.logsWithIndexByAttemptUid[this.attemptUid(taskRunId, this.selectedAttemptNumberByTaskRunId[taskRunId])])
|
||||
shouldDisplayProgressBar(taskRun) {
|
||||
return this.taskType(taskRun) === "io.kestra.core.tasks.flows.ForEachItem"
|
||||
},
|
||||
followExecution(executionId) {
|
||||
this.$store
|
||||
.dispatch("execution/followExecution", {id: executionId})
|
||||
.then(sse => {
|
||||
this.executionSSE = sse;
|
||||
this.executionSSE.onmessage = async (event) => {
|
||||
this.followedExecution = JSON.parse(event.data);
|
||||
}
|
||||
});
|
||||
shouldDisplayLogs(taskRun) {
|
||||
return (this.taskRunId ||
|
||||
(this.shownAttemptsUid.includes(this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])) &&
|
||||
this.logsWithIndexByAttemptUid[this.attemptUid(taskRun.id, this.selectedAttemptNumberByTaskRunId[taskRun.id])])) &&
|
||||
this.showLogs &&
|
||||
this.taskType(taskRun) !== "io.kestra.core.tasks.flows.ForEachItem"
|
||||
},
|
||||
followLogs(executionId) {
|
||||
this.$store
|
||||
@@ -471,20 +343,11 @@
|
||||
}
|
||||
})
|
||||
},
|
||||
swapDisplayedAttempt(taskRunId, newDisplayedAttemptUid) {
|
||||
this.shownAttemptsUid = this.shownAttemptsUid.map(attemptUid => attemptUid.startsWith(`${taskRunId}-`)
|
||||
? this.attemptUid(taskRunId, newDisplayedAttemptUid)
|
||||
: attemptUid
|
||||
);
|
||||
},
|
||||
isSubflow(taskRun) {
|
||||
return taskRun.outputs?.executionId;
|
||||
},
|
||||
selectedAttempt(taskRun) {
|
||||
return this.attempts(taskRun)[this.selectedAttemptNumberByTaskRunId[taskRun.id] ?? 0];
|
||||
},
|
||||
expandAll() {
|
||||
if(!this.followedExecution) {
|
||||
if (!this.followedExecution) {
|
||||
setTimeout(() => this.expandAll(), 50);
|
||||
return;
|
||||
}
|
||||
@@ -498,9 +361,9 @@
|
||||
this.expandSubflows();
|
||||
},
|
||||
expandSubflows() {
|
||||
if(this.currentTaskRuns.some(taskRun => this.isSubflow(taskRun))){
|
||||
if (this.currentTaskRuns.some(taskRun => this.isSubflow(taskRun))) {
|
||||
const subflowLogsElements = this.$refs["subflows-logs"];
|
||||
if(!subflowLogsElements || subflowLogsElements.length === 0) {
|
||||
if (!subflowLogsElements || subflowLogsElements.length === 0) {
|
||||
setTimeout(() => this.expandSubflows(), 50);
|
||||
}
|
||||
|
||||
@@ -525,38 +388,16 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
downloadContent(currentTaskRunId) {
|
||||
const params = this.params
|
||||
this.$store.dispatch("execution/downloadLogs", {
|
||||
executionId: this.followedExecution.id,
|
||||
params: {...params, taskRunId: currentTaskRunId}
|
||||
}).then((response) => {
|
||||
const url = window.URL.createObjectURL(new Blob([response]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", this.downloadName(currentTaskRunId));
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
});
|
||||
},
|
||||
downloadName(currentTaskRunId) {
|
||||
return `kestra-execution-${this.$moment().format("YYYYMMDDHHmmss")}-${this.followedExecution.id}-${currentTaskRunId}.log`
|
||||
},
|
||||
forwardEvent(type, event) {
|
||||
this.$emit(type, event);
|
||||
},
|
||||
uniqueTaskRunDisplayFilter(currentTaskRun) {
|
||||
return !(this.taskRunId && this.taskRunId !== currentTaskRun.id);
|
||||
},
|
||||
taskIcon(taskRun) {
|
||||
const task = FlowUtils.findTaskById(this.flow, taskRun.taskId);
|
||||
const parentTaskRunId = taskRun.parentTaskRunId;
|
||||
if(task === undefined && parentTaskRunId) {
|
||||
return this.taskIcon(this.taskRunById[parentTaskRunId])
|
||||
}
|
||||
return task ? task.type : undefined;
|
||||
},
|
||||
loadLogs(executionId) {
|
||||
if(!this.showLogs) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch("execution/loadLogs", {
|
||||
executionId,
|
||||
params: {
|
||||
@@ -576,6 +417,20 @@
|
||||
},
|
||||
toggleShowAttempt(attemptUid) {
|
||||
this.shownAttemptsUid = _xor(this.shownAttemptsUid, [attemptUid])
|
||||
},
|
||||
swapDisplayedAttempt(taskRunId, newDisplayedAttemptUid) {
|
||||
this.shownAttemptsUid = this.shownAttemptsUid.map(attemptUid => attemptUid.startsWith(`${taskRunId}-`)
|
||||
? this.attemptUid(taskRunId, newDisplayedAttemptUid)
|
||||
: attemptUid
|
||||
);
|
||||
},
|
||||
taskType(taskRun) {
|
||||
const task = FlowUtils.findTaskById(this.flow, taskRun.taskId);
|
||||
const parentTaskRunId = taskRun.parentTaskRunId;
|
||||
if (task === undefined && parentTaskRunId) {
|
||||
return this.taskType(this.taskRunById[parentTaskRunId])
|
||||
}
|
||||
return task ? task.type : undefined;
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -609,80 +464,6 @@
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.attempt-header {
|
||||
display: flex;
|
||||
gap: calc(var(--spacer) / 2);
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.attempt-number {
|
||||
background: var(--bs-gray-400);
|
||||
padding: .375rem .75rem;
|
||||
white-space: nowrap;
|
||||
|
||||
html.dark & {
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.task-id, .task-duration {
|
||||
padding: .375rem 0;
|
||||
}
|
||||
|
||||
.task-id {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
span span {
|
||||
color: var(--bs-tertiary-color);
|
||||
|
||||
html:not(.dark) & {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 36px;
|
||||
padding: 6px;
|
||||
border-radius: $border-radius-lg;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--bs-gray-600);
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: var(--font-size-xs)
|
||||
}
|
||||
|
||||
.task-duration small {
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--bs-gray-800);
|
||||
}
|
||||
|
||||
.more-dropdown-button {
|
||||
padding: .5rem;
|
||||
border: 1px solid rgba($white, .05);
|
||||
|
||||
&:not(:hover) {
|
||||
background: rgba($white, .10);
|
||||
}
|
||||
}
|
||||
|
||||
.expand-collapse {
|
||||
background-color: transparent !important;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attempt-wrapper {
|
||||
margin-bottom: var(--spacer);
|
||||
background-color: var(--bs-white);
|
||||
@@ -521,7 +521,8 @@
|
||||
"row count": "Row count",
|
||||
"encoding": "Encoding",
|
||||
"show": "Show",
|
||||
"advanced configuration": "Advanced configuration"
|
||||
"advanced configuration": "Advanced configuration",
|
||||
"all executions": "All executions"
|
||||
},
|
||||
"fr": {
|
||||
"id": "Identifiant",
|
||||
@@ -1035,7 +1036,8 @@
|
||||
"show": "Afficher",
|
||||
"advanced configuration": "Configuration avancée",
|
||||
"row count": "Nombre de lignes",
|
||||
"encoding": "Encodage"
|
||||
"encoding": "Encodage",
|
||||
"all executions": "Toutes les exécutions"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ export default class State {
|
||||
return {
|
||||
key: state.name,
|
||||
icon: state.icon,
|
||||
color: cssVariable("--bs-" + state.colorClass)
|
||||
color: cssVariable("--bs-" + state.colorClass),
|
||||
colorClass: state.colorClass
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user