Skip to content

Commit

Permalink
feat(core): internal storage file preview (#1770)
Browse files Browse the repository at this point in the history
close #1767
  • Loading branch information
Skraye authored Aug 17, 2023
1 parent bcad755 commit eab705b
Show file tree
Hide file tree
Showing 14 changed files with 383 additions and 34 deletions.
25 changes: 25 additions & 0 deletions ui/src/components/ListPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<template>
<el-table :data="value" stripe>
<el-table-column v-for="(column, index) in generateTableColumns" :key="index" :prop="column" :label="column" />
</el-table>
</template>
<script>
export default {
name: "ListPreview",
props: {
value: {
type: Array,
required: true
}
},
computed: {
generateTableColumns() {
return Object.keys(this.value[0]);
}
}
}
</script>

<style scoped lang="scss">
</style>
16 changes: 13 additions & 3 deletions ui/src/components/executions/ExecutionOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@
<var-value :execution="execution" :value="scope.row.output" />
</template>
</el-table-column>

<el-table-column column-key="action" class-name="row-action">
<template #default="scope">
<FilePreview v-if="scope.row.output.startsWith('kestra:///')" :value="scope.row.output" :execution-id="execution.id" />
</template>
</el-table-column>
</el-table>
<pagination :total="outputs.length" :page="page" :size="size" @page-changed="onPageChanged" />
</div>
Expand All @@ -96,9 +102,11 @@
import Editor from "../../components/inputs/Editor.vue";
import Collapse from "../layout/Collapse.vue";
import Pagination from "../layout/Pagination.vue";
import FilePreview from "./FilePreview.vue";
export default {
components: {
FilePreview,
Pagination,
VarValue,
Editor,
Expand All @@ -113,7 +121,9 @@
debugStackTrace: "",
isModalOpen: false,
size: this.$route.query.size ? this.$route.query.size : 25,
page: this.$route.query.page ? this.$route.query.page : 1
page: this.$route.query.page ? this.$route.query.page : 1,
isPreviewOpen: false,
selectedPreview: null
};
},
created() {
Expand Down Expand Up @@ -171,10 +181,10 @@
page: item.page
}
});
},
}
},
computed: {
...mapState("execution", ["execution"]),
...mapState("execution", ["execution", "filePreview"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
Expand Down
87 changes: 87 additions & 0 deletions ui/src/components/executions/FilePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<EyeOutline role="button" @click="getFilePreview(value)" />
<el-drawer
v-if="selectedPreview === value && filePreview"
v-model="isPreviewOpen"
destroy-on-close
lock-scroll
size=""
:append-to-body="true"
>
<template #header>
<h3>{{ $t("preview") }}</h3>
</template>
<template #default>
<list-preview v-if="filePreview.type === 'LIST'" :value="filePreview.content" />
<img v-else-if="filePreview.type === 'IMAGE'" :src="imageContent" alt="Image output preview">
<markdown v-else-if="filePreview.type === 'MARKDOWN'" :source="filePreview.content" />
<editor v-else :model-value="filePreview.content" :lang="extensionToMonacoLang" read-only />
</template>
</el-drawer>
</template>
<script>
import Editor from "../inputs/Editor.vue";
import ListPreview from "../ListPreview.vue";
import EyeOutline from "vue-material-design-icons/EyeOutline.vue";
import {mapState} from "vuex";
import Markdown from "../layout/Markdown.vue";
export default {
components: {Markdown, EyeOutline, ListPreview, Editor},
props: {
value: {
type: String,
required: true
},
executionId: {
type: String,
required: true
}
},
data() {
return {
isPreviewOpen: false,
selectedPreview: null
}
},
computed: {
...mapState("execution", ["filePreview"]),
extensionToMonacoLang() {
switch (this.filePreview.extension) {
case "json":
return "json";
case "jsonl":
return "jsonl";
case "yaml":
case "yml":
case "ion":
// little hack to get ion colored with monaco
return "yaml";
case "csv":
return "csv";
case "py":
return "python"
default:
return this.filePreview.extension;
}
},
imageContent() {
return "data:image/" + this.extension + ";base64," + this.filePreview.content;
}
},
methods: {
getFilePreview(path) {
this.selectedPreview = path;
this.$store
.dispatch("execution/filePreview", {
executionId: this.executionId,
path: path
})
.then(() => {
this.isPreviewOpen = true;
});
},
}
}
</script>
2 changes: 1 addition & 1 deletion ui/src/components/executions/VarValue.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<a class="el-button el-button--primary mt-2 mb-2 " v-if="isFile(value)" :href="itemUrl(value)" target="_blank">
<a class="el-button el-button--small el-button--primary" v-if="isFile(value)" :href="itemUrl(value)" target="_blank">
<Download />
&nbsp;
{{ $t('download') }}
Expand Down
12 changes: 11 additions & 1 deletion ui/src/components/executions/Vars.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<el-table stripe table-layout="auto" fixed :data="variables" size="small">
<el-table stripe table-layout="auto" fixed :data="variables">
<el-table-column prop="key" rowspan="3" :label="$t('name')">
<template #default="scope">
<code>{{ scope.row.key }}</code>
Expand All @@ -20,6 +20,12 @@
</template>
</template>
</el-table-column>

<el-table-column column-key="action" class-name="row-action">
<template #default="scope">
<FilePreview v-if="scope.row.value.startsWith('kestra:///')" :value="scope.row.value" :execution-id="execution.id" />
</template>
</el-table-column>
</el-table>
</template>

Expand All @@ -28,9 +34,12 @@
import VarValue from "./VarValue.vue";
import DateAgo from "../../components/layout/DateAgo.vue";
import SubFlowLink from "../flows/SubFlowLink.vue"
import FilePreview from "./FilePreview.vue";
import {mapState} from "vuex";
export default {
components: {
FilePreview,
DateAgo,
VarValue,
SubFlowLink
Expand All @@ -47,6 +56,7 @@
},
},
computed: {
...mapState("execution", ["execution"]),
variables() {
return Utils.executionVars(this.data);
},
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/inputs/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
readOnly: {type: Boolean, default: false},
lineNumbers: {type: Boolean, default: undefined},
minimap: {type: Boolean, default: false},
creating: {type: Boolean, default: false},
creating: {type: Boolean, default: false}
},
components: {
MonacoEditor,
Expand Down
11 changes: 11 additions & 0 deletions ui/src/stores/executions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
},
metrics: [],
metricsTotal: 0,
filePreview: undefined
},
actions: {
loadExecutions({commit}, options) {
Expand Down Expand Up @@ -143,6 +144,13 @@ export default {
}).then(response => {
return response.data
})
},
filePreview({commit}, options) {
return this.$http.get(`api/v1/executions/${options.executionId}/file/preview`, {
params: options
}).then(response => {
commit("setFilePreview", response.data)
})
}
},
mutations: {
Expand Down Expand Up @@ -179,6 +187,9 @@ export default {
},
setMetricsTotal(state, metrics) {
state.metricsTotal = metrics
},
setFilePreview(state, filePreview) {
state.filePreview = filePreview
}
},
getters: {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@
"slack support": "Ask help on our Slack",
"error detected": "Error detected",
"cannot swap tasks": "Can't swap the tasks",
"preview": "Preview",
"open": "Open",
"dependency task": "Task {taskId} is a dependency of another task",
"administration": "Administration",
"evaluation lock date": "Evaluation lock date",
Expand Down Expand Up @@ -942,6 +944,8 @@
"slack support": "Demandez de l'aide sur notre Slack",
"error detected": "Erreur détectée",
"cannot swap tasks": "Impossible de permuter les tâches",
"preview": "Aperçu",
"open": "Afficher",
"dependency task": "La tâche {taskId} est une dépendance d'une autre tâche",
"administration": "Administration",
"evaluation lock date": "Date verrou évaluation",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
package io.kestra.webserver.controllers;

import io.kestra.core.events.CrudEvent;
import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.exceptions.InternalException;
import io.kestra.core.models.Label;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.ExecutionKilled;
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.hierarchies.FlowGraph;
import io.kestra.core.models.storage.FileMetas;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.types.Webhook;
import io.kestra.core.models.validations.ManualConstraintViolation;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.runners.RunContext;
import io.kestra.core.runners.RunContextFactory;
import io.kestra.core.runners.RunnerUtils;
import io.kestra.core.services.ConditionService;
import io.kestra.core.services.ExecutionService;
import io.kestra.core.storages.StorageInterface;
import io.kestra.core.utils.Await;
import io.kestra.webserver.responses.BulkErrorResponse;
import io.kestra.webserver.responses.BulkResponse;
import io.kestra.webserver.responses.PagedResults;
import io.kestra.webserver.utils.PageableUtils;
import io.kestra.webserver.utils.RequestUtils;
import io.kestra.webserver.utils.filepreview.FileRender;
import io.kestra.webserver.utils.filepreview.FileRenderBuilder;
import io.micronaut.context.annotation.Value;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.convert.format.Format;
import io.micronaut.data.model.Pageable;
import io.micronaut.http.*;
Expand All @@ -31,40 +57,16 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.*;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import io.kestra.core.events.CrudEvent;
import io.kestra.core.events.CrudEventType;
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.executions.ExecutionKilled;
import io.kestra.core.models.executions.TaskRun;
import io.kestra.core.models.storage.FileMetas;
import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.State;
import io.kestra.core.models.hierarchies.FlowGraph;
import io.kestra.core.models.triggers.AbstractTrigger;
import io.kestra.core.models.triggers.types.Webhook;
import io.kestra.core.queues.QueueFactoryInterface;
import io.kestra.core.queues.QueueInterface;
import io.kestra.core.repositories.ExecutionRepositoryInterface;
import io.kestra.core.repositories.FlowRepositoryInterface;
import io.kestra.core.runners.RunnerUtils;
import io.kestra.core.services.ConditionService;
import io.kestra.core.services.ExecutionService;
import io.kestra.core.storages.StorageInterface;
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.IOException;
import java.io.InputStream;
import java.net.URI;
Expand Down Expand Up @@ -967,4 +969,23 @@ public Flowable<Event<Execution>> follow(
}
});
}

@ExecuteOn(TaskExecutors.IO)
@Get(uri = "executions/{executionId}/file/preview", produces = MediaType.APPLICATION_JSON)
@Operation(tags = {"Executions"}, summary = "Get file preview for an execution")
public HttpResponse<?> filePreview(
@Parameter(description = "The execution id") @PathVariable String executionId,
@Parameter(description = "The internal storage uri") @QueryValue URI path
) throws IOException {
HttpResponse<StreamedFile> httpResponse = this.validateFile(executionId, path, "/api/v1/executions/{executionId}/file?path=" + path);
if (httpResponse != null) {
return httpResponse;
}

String extension = FilenameUtils.getExtension(path.toString());
InputStream fileStream = storageInterface.get(path);

FileRender fileRender = FileRenderBuilder.of(extension, fileStream);
return HttpResponse.ok(fileRender);
}
}
Loading

0 comments on commit eab705b

Please sign in to comment.