Skip to content

Commit

Permalink
feat(ui): display and download execution inputs and outputs (#39)
Browse files Browse the repository at this point in the history
close #29
  • Loading branch information
eric regnier authored and tchiotludo committed Jan 22, 2020
1 parent 335d496 commit 1010f83
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 13 deletions.
69 changes: 69 additions & 0 deletions ui/src/components/executions/ExecutionOutput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<div v-if="execution && outputs">
<hr />
<h2>{{$t('outputs') | cap}}</h2>
<b-table
responsive="xl"
striped
hover
bordered
:fields="fields"
:items="outputs"
class="mb-0"
>
<template v-slot:cell(value)="row">
<b-link
class="btn btn-primary"
v-if="row.item.download"
target="_blank"
:href="itemUrl({uri: row.item.value})"
>{{$t('download') | cap}}</b-link>
<span v-else>{{row.item.value}}</span>
</template>
</b-table>
</div>
</template>
<script>
import { mapState } from "vuex";
import { apiRoot } from "../../http";
export default {
methods: {
itemUrl(value) {
return `${apiRoot}executions/${this.execution.id}/file?filePath=${value.uri}&type=${value.type}`;
}
},
computed: {
...mapState("execution", ["execution"]),
fields() {
return [
{
key: "key",
label: this.$t("name")
},
{
key: "task",
label: this.$t("task")
},
{
key: "value",
label: this.$t("value")
}
];
},
outputs() {
const outputs = [];
for (const task of this.execution.taskRunList) {
for (const key in task.outputs) {
const item = { key, value: task.outputs[key], task: task.id };
if (task.outputs[key].startsWith("kestra:///")) {
item.download = true;
}
outputs.push(item);
}
}
return outputs;
}
}
};
</script>
8 changes: 7 additions & 1 deletion ui/src/components/executions/ExecutionRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Gantt from "./Gantt";
import Overview from "./Overview";
import Logs from "./Logs";
import Topology from "./Topology";
import ExecutionOutput from "./ExecutionOutput";
import Trigger from "vue-material-design-icons/Cogs";
import BottomLine from "../layout/BottomLine";
import FlowActions from "../flows/FlowActions";
Expand All @@ -35,7 +36,8 @@ export default {
Gantt,
Logs,
Topology,
FlowActions
FlowActions,
ExecutionOutput
},
data() {
return {
Expand Down Expand Up @@ -122,6 +124,10 @@ export default {
{
tab: "topology",
title: title("topology")
},
{
tab: "execution-output",
title: title("output")
}
];
}
Expand Down
79 changes: 67 additions & 12 deletions ui/src/components/executions/Overview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,98 @@
<status :status="execution.state.current" />
</b-col>
</b-row>
<h2>{{$t('execution') | cap}}</h2>
<b-table responsive="xl" striped hover bordered :items="items" class="mb-0"></b-table>
<div v-if="execution.inputs">
<hr />
<h2>{{$t('inputs') | cap}}</h2>
<b-table
responsive="xl"
striped
hover
bordered
:items="inputs"
:fields="fields"
class="mb-0"
>
<template v-slot:cell(value)="row">
<span
v-if="['string', 'optional', 'float', 'int', 'instant'].includes(row.item.key)"
>{{row.item.value}}</span>
<b-link
class="btn btn-primary"
v-if="['optionalFile', 'file'].includes(row.item.key)"
target="_blank"
:href="itemUrl(row.item.value)"
>{{$t('download') | cap}}</b-link>
</template>
</b-table>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import Status from "../Status";
import humanizeDuration from "humanize-duration";
import { apiRoot } from "../../http";
const ts = date => new Date(date).getTime();
export default {
components: {
Status
},
methods: {
itemUrl(value) {
return `${apiRoot}executions/${this.execution.id}/file?filePath=${value.uri}&type=${value.type}`;
}
},
computed: {
...mapState("execution", ["execution"]),
fields() {
return [
{
key: "key",
label: this.$t("name")
},
{
key: "value",
label: this.$t("value")
}
];
},
items() {
const startTs = this.execution.state.histories[0].date;
const stopTs = this.execution.state.histories[this.execution.state.histories.length - 1].date;
const stopTs = this.execution.state.histories[
this.execution.state.histories.length - 1
].date;
const delta = ts(stopTs) - ts(startTs);
const duration = this.$moment.duration(delta);
const humanDuration = humanizeDuration(duration);
const stepCount = this.execution.taskRunList ? this.execution.taskRunList.length : 0
const stepCount = this.execution.taskRunList
? this.execution.taskRunList.length
: 0;
return [
{key: this.$t('namespace'), value: this.execution.namespace},
{key: this.$t('flow'), value: this.execution.flowId},
{key: this.$t('created date'), value: startTs},
{key: this.$t('updated date'), value: stopTs},
{key: this.$t('duration'), value: humanDuration},
{key: this.$t('steps'), value: stepCount},
]
{ key: this.$t("namespace"), value: this.execution.namespace },
{ key: this.$t("flow"), value: this.execution.flowId },
{ key: this.$t("created date"), value: startTs },
{ key: this.$t("updated date"), value: stopTs },
{ key: this.$t("duration"), value: humanDuration },
{ key: this.$t("steps"), value: stepCount }
];
},
inputs() {
const inputs = [];
for (const key in this.execution.inputs) {
inputs.push({ key, value: this.execution.inputs[key] });
}
return inputs;
}
}
};
</script>
<style scoped lang="scss">
/deep/ thead {
display: none;
}
/deep/ thead {
display: none;
}
</style>
3 changes: 3 additions & 0 deletions ui/src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ export default callback => {
Vue.axios.defaults.baseURL = (process.env.VUE_APP_API_URL || "") + "/";
callback();
};


export const apiRoot = `${process.env.VUE_APP_API_URL}/api/v1/`
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
package org.kestra.webserver.controllers;

import io.micronaut.data.model.Pageable;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.http.multipart.StreamingFileUpload;
import io.micronaut.http.server.types.files.StreamedFile;
import io.micronaut.http.sse.Event;
import io.micronaut.validation.Validated;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import org.apache.commons.io.FilenameUtils;
import org.kestra.core.models.executions.Execution;
import org.kestra.core.models.flows.Flow;
import org.kestra.core.queues.QueueFactoryInterface;
import org.kestra.core.queues.QueueInterface;
import org.kestra.core.repositories.ExecutionRepositoryInterface;
import org.kestra.core.repositories.FlowRepositoryInterface;
import org.kestra.core.runners.RunnerUtils;
import org.kestra.core.storages.StorageInterface;
import org.kestra.core.storages.StorageObject;
import org.kestra.webserver.responses.PagedResults;
import org.kestra.webserver.utils.PageableUtils;
import org.reactivestreams.Publisher;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -45,6 +56,9 @@ public class ExecutionController {
@Inject
private RunnerUtils runnerUtils;

@Inject
private StorageInterface storageInterface;

@Inject
@Named(QueueFactoryInterface.EXECUTION_NAMED)
protected QueueInterface<Execution> executionQueue;
Expand Down Expand Up @@ -126,6 +140,30 @@ public Maybe<Execution> trigger(
return Maybe.just(current);
}

/**
* Download file binary from uri parameter
*
* @param filePath The file URI to return
* @param type The file storage type
* @return data binary content
*/
@Get(uri = "executions/{executionId}/file", produces = MediaType.APPLICATION_OCTET_STREAM)
public Maybe<StreamedFile> file(
String executionId,
@QueryValue(value = "filePath") URI filePath,
@QueryValue(value = "type") String type
) throws URISyntaxException, IOException {
Optional<Execution> execution = executionRepository.findById(executionId);
if (execution.isEmpty()) {
return Maybe.empty();
}

InputStream fileHandler = storageInterface.get(filePath);
return Maybe.just(new StreamedFile(fileHandler, MediaType.APPLICATION_OCTET_STREAM_TYPE)
.attach(FilenameUtils.getName(filePath.toString()))
);
}

/**
* Trigger an new execution for current flow and follow execution
*
Expand Down

0 comments on commit 1010f83

Please sign in to comment.