Skip to content

Commit

Permalink
feat(ui): add a eval expression on outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
tchiotludo committed Aug 1, 2022
1 parent 5a259fd commit df2ec8a
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mitchellbosecke.pebble.extension.writer.SpecializedWriter;
import io.kestra.core.serializers.JacksonMapper;
import lombok.SneakyThrows;

import java.io.IOException;
Expand All @@ -11,7 +12,7 @@
import java.util.Map;

public class JsonWriter extends Writer implements SpecializedWriter {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final ObjectMapper MAPPER = JacksonMapper.ofJson();

private final StringWriter stringWriter;

Expand Down
75 changes: 68 additions & 7 deletions ui/src/components/executions/ExecutionOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@
:options="selectOptions"
:placeholder="$t('display output for specific task') + '...'"
/>
<span id="debug-btn-wrapper">
<b-btn
:disabled="!filter"
v-b-modal="`debug-expression-modal`"
>
{{ $t("eval.title") }}
</b-btn>
</span>
<b-tooltip
v-if="!filter"
placement="right"
target="debug-btn-wrapper"
>
{{ $t("eval.tooltip") }}
</b-tooltip>
</b-nav-form>
</b-collapse>
</b-navbar>
Expand Down Expand Up @@ -40,21 +55,55 @@
<var-value :execution="execution" :value="row.item.output" />
</template>
</b-table>

<b-modal
hide-backdrop
id="debug-expression-modal"
modal-class="right"
size="lg"
>
<template #modal-header>
<h5>{{ $t("eval.title") }}</h5>
</template>

<template>
<editor class="mb-2" ref="editorContainer" :full-height="false" @onSave="onDebugExpression(filter, $event)" :input="true" :navbar="false" value="" />
<pre v-if="debugExpression">{{ debugExpression }}</pre>
<b-alert class="debug-error" variant="danger" show v-if="debugError">
<p><strong>{{ debugError }}</strong></p>
<pre class="mb-0">{{ debugStackTrace }}</pre>
</b-alert>
</template>

<template #modal-footer>
<b-button
variant="secondary"
@click="onDebugExpression(filter, $refs.editorContainer.editor.getValue())"
>
{{ $t("eval.title") }}
</b-button>
</template>
</b-modal>
</div>
</template>
<script>
import {mapState} from "vuex";
import md5 from "md5";
import VarValue from "./VarValue";
import Utils from "../../utils/utils";
import Editor from "../../components/inputs/Editor";
import Vue from "vue"
export default {
components: {
VarValue,
Editor,
},
data() {
return {
filter: ""
filter: "",
debugExpression: "",
debugError: "",
debugStackTrace: "",
};
},
created() {
Expand All @@ -75,20 +124,32 @@
const newRoute = {query: {...this.$route.query}};
newRoute.query.search = this.filter;
this.$router.push(newRoute);
} else {
const newRoute = {query: {...this.$route.query}};
delete newRoute.query.search;
this.$router.push(newRoute);
}
},
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""));
onDebugExpression(taskRunId, expression) {
Vue.axios.post(`/api/v1/executions/${this.execution.id}/eval/${taskRunId}`, expression, {
headers: {
"Content-type": "text/plain",
}
}).then(response => {
this.debugExpression = response.data.result;
this.debugError = response.data.error;
this.debugStackTrace = response.data.stackTrace;
})
}
},
computed: {
...mapState("execution", ["execution"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
options[this.taskRunOutputToken(taskRun)] = {
options[taskRun.id] = {
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""),
value: this.taskRunOutputToken(taskRun)
value: taskRun.id
}
}
Expand Down Expand Up @@ -117,7 +178,7 @@
outputs() {
const outputs = [];
for (const taskRun of this.execution.taskRunList || []) {
const token = this.taskRunOutputToken(taskRun)
const token = taskRun.id;
if (!this.filter || token === this.filter) {
Utils.executionVars(taskRun.outputs).forEach(output => {
const item = {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/inputs/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
}
},
updateSize(width, height) {
if (width === 0 || height === 0) {
if (width <= 0 || height <= 0) {
window.setTimeout(this.onResize, 100);
} else {
this.editor.layout({width: width, height: height});
Expand Down Expand Up @@ -307,7 +307,7 @@
&.single-line {
padding: $input-padding-y $input-padding-x;
background: white;
border: 1px solid $input-border-color;
border: 1px solid var(--input-border-color);
min-height: 38px;
&.theme-vs-dark {
Expand Down
1 change: 1 addition & 0 deletions ui/src/styles/layout/root.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
--table-border-color: #{$table-border-color};
--body-bg: #{$body-bg};
--body-color: #{$body-color};
--input-border-color: #{$input-border-color};
}

8 changes: 8 additions & 0 deletions ui/src/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@
"no data current task": "No data available for current task",
"outputs": "Outputs",
"output": "Output",
"eval": {
"title": "Eval expression",
"tooltip": "You need to choose a task to eval an expression."
},
"attempt": "Attempt",
"toggle output display": "Toggle output display",
"name": "Name",
Expand Down Expand Up @@ -371,6 +375,10 @@
"no data current task": "Aucune données pour cette tache",
"outputs": "Sorties",
"output": "Sortie",
"eval": {
"title": "Évaluer une expression",
"tooltip": "Vous devez choisir une tâche pour évaluer une expression."
},
"attempt": "Essai",
"toggle output display": "Permuter l'affichage de sortie",
"name": "Nom",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.kestra.webserver.controllers;

import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.runners.RunContext;
import io.kestra.core.runners.RunContextFactory;
import io.micronaut.context.annotation.Value;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.core.convert.format.Format;
Expand All @@ -21,6 +25,8 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.apache.commons.io.FilenameUtils;
import io.kestra.core.events.CrudEvent;
import io.kestra.core.events.CrudEventType;
Expand All @@ -45,12 +51,13 @@
import io.kestra.core.utils.Await;
import io.kestra.webserver.responses.PagedResults;
import io.kestra.webserver.utils.PageableUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.reactivestreams.Publisher;

import io.micronaut.core.annotation.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.FileNotFoundException;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
Expand Down Expand Up @@ -98,6 +105,9 @@ public class ExecutionController {
@Inject
private ApplicationEventPublisher<CrudEvent<Execution>> eventPublisher;

@Inject
private RunContextFactory runContextFactory;

@ExecuteOn(TaskExecutors.IO)
@Get(uri = "executions/search", produces = MediaType.TEXT_JSON)
@Operation(tags = {"Executions"}, summary = "Search for executions")
Expand Down Expand Up @@ -177,6 +187,49 @@ public FlowGraph flowGraph(
.orElse(null);
}

@ExecuteOn(TaskExecutors.IO)
@Post(uri = "executions/{executionId}/eval/{taskRunId}", produces = MediaType.TEXT_JSON, consumes = MediaType.TEXT_PLAIN)
@Operation(tags = {"Executions"}, summary = "Evaluate a variable expression for this taskrun")
public EvalResult eval(
@Parameter(description = "The execution id") String executionId,
@Parameter(description = "The taskrun id") String taskRunId,
@Body String expression
) throws InternalException {
Execution execution = executionRepository
.findById(executionId)
.orElseThrow(() -> new NoSuchElementException("Unable to find execution '" + executionId + "'"));

TaskRun taskRun = execution
.findTaskRunByTaskRunId(taskRunId);

Flow flow = flowRepository
.findByExecution(execution);

Task task = flow.findTaskByTaskId(taskRun.getTaskId());

RunContext runContext = runContextFactory.of(flow, task, execution, taskRun);

try {
return EvalResult.builder()
.result(runContext.render(expression))
.build();
} catch (IllegalVariableEvaluationException e) {
return EvalResult.builder()
.error(e.getMessage())
.stackTrace(ExceptionUtils.getStackTrace(e))
.build();
}
}

@SuperBuilder
@Getter
@NoArgsConstructor
public static class EvalResult {
String result;
String error;
String stackTrace;
}

@ExecuteOn(TaskExecutors.IO)
@Get(uri = "executions/{executionId}", produces = MediaType.TEXT_JSON)
@Operation(tags = {"Executions"}, summary = "Get an execution")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,39 @@ void triggerAndFollow() {
assertThat(results.get(results.size() - 1).getData().getState().getCurrent(), is(State.Type.SUCCESS));
}

private ExecutionController.EvalResult eval(Execution execution, String expression, int index) {
ExecutionController.EvalResult eval = client.toBlocking().retrieve(
HttpRequest
.POST(
"/api/v1/executions/" + execution.getId() + "/eval/" + execution.getTaskRunList().get(index).getId(),
expression
)
.contentType(MediaType.TEXT_PLAIN_TYPE),
Argument.of(ExecutionController.EvalResult.class)
);

return eval;
}

@Test
void eval() throws TimeoutException {
Execution execution = runnerUtils.runOne("io.kestra.tests", "each-sequential-nested");

ExecutionController.EvalResult result = this.eval(execution, "my simple string", 0);
assertThat(result.getResult(), is("my simple string"));

result = this.eval(execution, "{{ taskrun.id }}", 0);
assertThat(result.getResult(), is(execution.getTaskRunList().get(0).getId()));

result = this.eval(execution, "{{ outputs['1-1_return'][taskrun.value].value }}", 21);
assertThat(result.getResult(), containsString("1-1_return"));

result = this.eval(execution, "{{ missing }}", 21);
assertThat(result.getResult(), is(nullValue()));
assertThat(result.getError(), containsString("Missing variable: 'missing' on '{{ missing }}' at line 1"));
assertThat(result.getStackTrace(), containsString("Missing variable: 'missing' on '{{ missing }}' at line 1"));
}

@Test
void restartFromUnknownTaskId() throws TimeoutException {
final String flowId = "restart_with_inputs";
Expand Down

0 comments on commit df2ec8a

Please sign in to comment.