mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
5 Commits
feat/simul
...
feat/previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
769adad5e8 | ||
|
|
09c79f76d7 | ||
|
|
086fd2a4cb | ||
|
|
3f9a2d9a57 | ||
|
|
119bd51170 |
@@ -78,4 +78,11 @@ jobs:
|
||||
"new_version": "${{ github.ref_name }}",
|
||||
"github_repository": "${{ github.repository }}",
|
||||
"github_actor": "${{ github.actor }}"
|
||||
}
|
||||
}
|
||||
|
||||
- name: Merge Release Notes
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: ./actions/.github/actions/github-release-note-merge
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
|
||||
@@ -6,6 +6,8 @@ import io.kestra.core.plugins.PluginCatalogService;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.storages.StorageInterfaceFactory;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererFactory;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererRegistry;
|
||||
import io.micronaut.context.annotation.Bean;
|
||||
import io.micronaut.context.annotation.ConfigurationProperties;
|
||||
import io.micronaut.context.annotation.Factory;
|
||||
@@ -87,4 +89,9 @@ public class KestraBeansFactory {
|
||||
return (Map<String, Object>) storage.get(StringConvention.CAMEL_CASE.format(type));
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
public PreviewRendererFactory previewRendererFactory(final PluginRegistry pluginRegistry) {
|
||||
return new PreviewRendererFactory(pluginRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.utils.VersionProvider;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererRegistry;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.annotation.Context;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
@@ -82,6 +84,8 @@ public abstract class KestraContext {
|
||||
*/
|
||||
public abstract PluginRegistry getPluginRegistry();
|
||||
|
||||
public abstract PreviewRenderer getPreviewRenderer();
|
||||
|
||||
public abstract StorageInterface getStorageInterface();
|
||||
|
||||
/**
|
||||
@@ -107,8 +111,8 @@ public abstract class KestraContext {
|
||||
/**
|
||||
* Creates a new {@link KestraContext} instance.
|
||||
*
|
||||
* @param applicationContext The {@link ApplicationContext}.
|
||||
* @param environment The {@link Environment}.
|
||||
* @param applicationContext The {@link ApplicationContext}.
|
||||
* @param environment The {@link Environment}.
|
||||
*/
|
||||
public Initializer(ApplicationContext applicationContext,
|
||||
Environment environment) {
|
||||
@@ -118,7 +122,9 @@ public abstract class KestraContext {
|
||||
KestraContext.setContext(this);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public ServerType getServerType() {
|
||||
return Optional.ofNullable(environment)
|
||||
@@ -126,20 +132,27 @@ public abstract class KestraContext {
|
||||
.orElse(ServerType.STANDALONE);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public Optional<Integer> getWorkerMaxNumThreads() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_MAX_NUM_THREADS, Integer.class));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public Optional<String> getWorkerGroupKey() {
|
||||
return Optional.ofNullable(environment)
|
||||
.flatMap(env -> env.getProperty(KESTRA_WORKER_GROUP_KEY, String.class));
|
||||
}
|
||||
/** {@inheritDoc} **/
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public void injectWorkerConfigs(Integer maxNumThreads, String workerGroupKey) {
|
||||
final Map<String, Object> configs = new HashMap<>();
|
||||
@@ -154,7 +167,9 @@ public abstract class KestraContext {
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
if (isShutdown.compareAndSet(false, true)) {
|
||||
@@ -164,13 +179,17 @@ public abstract class KestraContext {
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} **/
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public PluginRegistry getPluginRegistry() {
|
||||
// Lazy init of the PluginRegistry.
|
||||
@@ -182,5 +201,11 @@ public abstract class KestraContext {
|
||||
// Lazy init of the PluginRegistry.
|
||||
return this.applicationContext.getBean(StorageInterface.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreviewRenderer getPreviewRenderer() {
|
||||
// Lazy init of the PreviewRenderer.
|
||||
return this.applicationContext.getBean(PreviewRenderer.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.kestra.core.models.tasks.runners.TaskRunner;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.secret.SecretPluginInterface;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -117,6 +118,7 @@ public class PluginScanner {
|
||||
List<Class<? extends AdditionalPlugin>> additionalPlugins = new ArrayList<>();
|
||||
List<String> guides = new ArrayList<>();
|
||||
Map<String, Class<?>> aliases = new HashMap<>();
|
||||
List<Class<? extends PreviewRenderer>> previewRenderers = new ArrayList<>();
|
||||
|
||||
if (manifest == null) {
|
||||
manifest = getManifest(classLoader);
|
||||
@@ -186,6 +188,11 @@ public class PluginScanner {
|
||||
log.debug("Loading additional plugin: '{}'", plugin.getClass());
|
||||
additionalPlugins.add(additionalPlugin.getClass());
|
||||
}
|
||||
case PreviewRenderer previewRenderer -> {
|
||||
log.info("Found PreviewRenderer: {}", plugin.getClass().getName());
|
||||
log.debug("Loading PreviewRenderer plugin: '{}'", plugin.getClass());
|
||||
previewRenderers.add(previewRenderer.getClass());
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
@@ -236,6 +243,7 @@ public class PluginScanner {
|
||||
e -> e.getKey().toLowerCase(),
|
||||
Function.identity()
|
||||
)))
|
||||
.previewRenderers(previewRenderers)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.kestra.core.models.tasks.runners.TaskRunner;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.secret.SecretPluginInterface;
|
||||
import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import lombok.*;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -46,6 +47,8 @@ public class RegisteredPlugin {
|
||||
public static final String DATA_FILTERS_KPI_GROUP_NAME = "data-filters-kpi";
|
||||
public static final String LOG_EXPORTERS_GROUP_NAME = "log-exporters";
|
||||
public static final String ADDITIONAL_PLUGINS_GROUP_NAME = "additional-plugins";
|
||||
public static final String PREVIEW_RENDERERS_GROUP_NAME = "preview-renderers";
|
||||
|
||||
|
||||
private final ExternalPlugin externalPlugin;
|
||||
private final Manifest manifest;
|
||||
@@ -63,6 +66,7 @@ public class RegisteredPlugin {
|
||||
private final List<Class<? extends DataFilterKPI<?, ?>>> dataFiltersKPI;
|
||||
private final List<Class<? extends LogExporter<?>>> logExporters;
|
||||
private final List<Class<? extends AdditionalPlugin>> additionalPlugins;
|
||||
private final List<Class<? extends PreviewRenderer>> previewRenderers;
|
||||
private final List<String> guides;
|
||||
// Map<lowercasealias, <Alias, Class>>
|
||||
private final Map<String, Map.Entry<String, Class<?>>> aliases;
|
||||
@@ -117,6 +121,10 @@ public class RegisteredPlugin {
|
||||
return StorageInterface.class;
|
||||
}
|
||||
|
||||
if (this.getPreviewRenderers().stream().anyMatch(r -> r.getName().equals(cls))) {
|
||||
return PreviewRenderer.class;
|
||||
}
|
||||
|
||||
if (this.getSecrets().stream().anyMatch(r -> r.getName().equals(cls))) {
|
||||
return SecretPluginInterface.class;
|
||||
}
|
||||
@@ -187,6 +195,7 @@ public class RegisteredPlugin {
|
||||
result.put(DATA_FILTERS_KPI_GROUP_NAME, Arrays.asList(this.getDataFiltersKPI().toArray(Class[]::new)));
|
||||
result.put(LOG_EXPORTERS_GROUP_NAME, Arrays.asList(this.getLogExporters().toArray(Class[]::new)));
|
||||
result.put(ADDITIONAL_PLUGINS_GROUP_NAME, Arrays.asList(this.getAdditionalPlugins().toArray(Class[]::new)));
|
||||
result.put(PREVIEW_RENDERERS_GROUP_NAME, Arrays.asList(this.getPreviewRenderers().toArray(Class[]::new)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import io.kestra.core.storages.StorageInterface;
|
||||
import io.kestra.core.storages.kv.KVStore;
|
||||
import io.kestra.core.utils.ListUtils;
|
||||
import io.kestra.core.utils.VersionProvider;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererRegistry;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
@@ -602,6 +604,8 @@ public class DefaultRunContext extends RunContext {
|
||||
private List<String> secretInputs;
|
||||
private Task task;
|
||||
private AbstractTrigger trigger;
|
||||
private PreviewRenderer previewRenderer;
|
||||
private PreviewRendererRegistry previewRendererRegistry;
|
||||
|
||||
/**
|
||||
* Builds the new {@link DefaultRunContext} object.
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.kestra.core.storages;
|
||||
|
||||
import io.kestra.core.utils.WindowsUtils;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
@@ -103,7 +102,7 @@ public record NamespaceFile(
|
||||
filePath = filePath.getRoot().relativize(filePath);
|
||||
}
|
||||
// Need to remove starting trailing slash for Windows
|
||||
String pathWithoutTrailingSlash = path.toString().replaceFirst("^[.]*[\\\\|/]*", "");
|
||||
String pathWithoutTrailingSlash = path.toString().replaceFirst("^[.]*[\\\\|/]+", "");
|
||||
|
||||
return new NamespaceFile(
|
||||
pathWithoutTrailingSlash,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
package io.kestra.plugin.core.preview;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Getter;
|
||||
@@ -18,7 +18,7 @@ public abstract class FileRender {
|
||||
@JsonInclude
|
||||
public boolean truncated = false;
|
||||
|
||||
FileRender(String extension, Integer maxLine) {
|
||||
protected FileRender(String extension, Integer maxLine) {
|
||||
this.maxLine = maxLine;
|
||||
this.extension = extension;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.kestra.plugin.core.preview;
|
||||
|
||||
import io.kestra.core.models.Plugin;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface for plugins to provide file preview rendering capabilities.
|
||||
* Plugins can implement this to support preview of specific file formats.
|
||||
*/
|
||||
public interface PreviewRenderer extends Plugin {
|
||||
|
||||
/**
|
||||
* File extensions this renderer supports (without dot, e.g., "parquet", "csv")
|
||||
*/
|
||||
List<String> supportedExtensions();
|
||||
|
||||
/**
|
||||
* Render preview for the given file
|
||||
*
|
||||
* @param extension file extension
|
||||
* @param fileStream input stream of the file
|
||||
* @param charset charset for text-based files (optional)
|
||||
* @param maxLines maximum number of lines/records to preview
|
||||
* @return PreviewResult object containing preview data
|
||||
* @throws IOException if file cannot be read or parsed
|
||||
*/
|
||||
PreviewResult render(String extension, InputStream fileStream, Optional<Charset> charset, Integer maxLines) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package io.kestra.plugin.core.preview;
|
||||
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
import io.kestra.core.plugins.RegisteredPlugin;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class PreviewRendererFactory {
|
||||
|
||||
private final PluginRegistry pluginRegistry;
|
||||
private Map<String, Class<? extends PreviewRenderer>> rendererClasses;
|
||||
|
||||
public PreviewRendererFactory(PluginRegistry pluginRegistry) {
|
||||
this.pluginRegistry = pluginRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview renderer for given file extension
|
||||
*/
|
||||
public Optional<PreviewRenderer> getRenderer(String extension) {
|
||||
log.info("Looking for preview renderer for extension: '{}'", extension);
|
||||
|
||||
if (rendererClasses == null) {
|
||||
log.info("Renderer classes not initialized, initializing now...");
|
||||
initializeRenderers();
|
||||
}
|
||||
|
||||
String normalizedExt = extension.toLowerCase();
|
||||
Class<? extends PreviewRenderer> rendererClass = rendererClasses.get(normalizedExt);
|
||||
|
||||
log.info("Available extensions: {}", rendererClasses.keySet());
|
||||
log.info("Looking for normalized extension: '{}', found class: {}", normalizedExt,
|
||||
rendererClass != null ? rendererClass.getName() : "null");
|
||||
|
||||
if (rendererClass == null) {
|
||||
log.warn("No preview renderer found for extension '{}'", extension);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
PreviewRenderer renderer = rendererClass.getDeclaredConstructor().newInstance();
|
||||
log.info("Successfully created preview renderer instance: {}", rendererClass.getSimpleName());
|
||||
return Optional.of(renderer);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to instantiate preview renderer for extension '{}': {}", extension, e.getMessage(), e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize renderers by discovering all PreviewRenderer plugins
|
||||
*/
|
||||
private void initializeRenderers() {
|
||||
rendererClasses = new HashMap<>();
|
||||
|
||||
log.info("Starting to initialize preview renderers...");
|
||||
|
||||
List<RegisteredPlugin> plugins = pluginRegistry.plugins().stream().toList();
|
||||
log.info("Found {} registered plugins", plugins.size());
|
||||
|
||||
plugins.forEach(plugin -> {
|
||||
List<Class<? extends PreviewRenderer>> renderers = plugin.getPreviewRenderers();
|
||||
log.info("Plugin '{}' has {} preview renderers", plugin.name(), renderers.size());
|
||||
renderers.forEach(rendererClass ->
|
||||
log.info(" - Preview renderer class: {}", rendererClass.getName())
|
||||
);
|
||||
});
|
||||
|
||||
pluginRegistry.plugins()
|
||||
.stream()
|
||||
.map(RegisteredPlugin::getPreviewRenderers)
|
||||
.flatMap(List::stream)
|
||||
.forEach(rendererClass -> {
|
||||
try {
|
||||
log.info("Trying to instantiate preview renderer: {}", rendererClass.getName());
|
||||
PreviewRenderer instance = rendererClass.getDeclaredConstructor().newInstance();
|
||||
List<String> extensions = instance.supportedExtensions();
|
||||
log.info("Preview renderer {} supports extensions: {}", rendererClass.getSimpleName(), extensions);
|
||||
|
||||
for (String extension : extensions) {
|
||||
String normalizedExt = extension.toLowerCase();
|
||||
rendererClasses.put(normalizedExt, rendererClass);
|
||||
log.info("Registered preview renderer for '{}': {}", normalizedExt, rendererClass.getSimpleName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to register preview renderer {}: {}", rendererClass.getSimpleName(), e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Initialization complete. Registered renderers for extensions: {}", rendererClasses.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all supported extensions
|
||||
*/
|
||||
public List<String> getSupportedExtensions() {
|
||||
if (rendererClasses == null) {
|
||||
initializeRenderers();
|
||||
}
|
||||
return List.copyOf(rendererClasses.keySet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.kestra.plugin.core.preview;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class PreviewRendererRegistry {
|
||||
|
||||
private final Map<String, PreviewRenderer> renderers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Register a preview renderer for specific file extensions
|
||||
*/
|
||||
public void register(PreviewRenderer renderer) {
|
||||
for (String extension : renderer.supportedExtensions()) {
|
||||
String normalizedExt = extension.toLowerCase();
|
||||
if (renderers.containsKey(normalizedExt)) {
|
||||
log.warn("Preview renderer for extension '{}' is being overridden by {}",
|
||||
normalizedExt, renderer.getClass().getSimpleName());
|
||||
}
|
||||
renderers.put(normalizedExt, renderer);
|
||||
log.debug("Registered preview renderer for '{}': {}", normalizedExt, renderer.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview renderer for given file extension
|
||||
*/
|
||||
public Optional<PreviewRenderer> getRenderer(String extension) {
|
||||
return Optional.ofNullable(renderers.get(extension.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if preview is available for given extension
|
||||
*/
|
||||
public boolean hasRenderer(String extension) {
|
||||
return renderers.containsKey(extension.toLowerCase());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.kestra.plugin.core.preview;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PreviewResult {
|
||||
public String extension;
|
||||
public Type type;
|
||||
public Object content;
|
||||
public Integer maxLines;
|
||||
|
||||
@JsonInclude
|
||||
public boolean truncated = false;
|
||||
|
||||
public enum Type {
|
||||
TEXT, LIST, IMAGE, MARKDOWN, PDF
|
||||
}
|
||||
}
|
||||
@@ -324,24 +324,24 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.el-cascader-panel {
|
||||
:deep(.el-cascader-panel) {
|
||||
min-height: 197px;
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
border-radius: 0;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: hidden !important;
|
||||
|
||||
:deep(.el-scrollbar.el-cascader-menu:nth-of-type(-n + 2) ul li:first-child) {
|
||||
.el-scrollbar.el-cascader-menu:nth-of-type(-n + 2) ul li:first-child {
|
||||
pointer-events: auto !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.el-cascader-node) {
|
||||
.el-cascader-node {
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
:deep(.el-cascader-panel__wrap) {
|
||||
.el-cascader-panel__wrap {
|
||||
overflow-x: auto !important;
|
||||
display: flex !important;
|
||||
min-width: max-content !important;
|
||||
@@ -360,7 +360,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& .el-cascader-node {
|
||||
.el-cascader-node {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: var(--el-font-size-small);
|
||||
|
||||
@@ -91,7 +91,6 @@
|
||||
onDebugExpression(
|
||||
editorValue.length > 0 ? editorValue : computedDebugValue,
|
||||
)
|
||||
|
||||
"
|
||||
class="mt-3 el-button--wrap"
|
||||
>
|
||||
@@ -153,24 +152,29 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, shallowRef, onMounted} from "vue";
|
||||
import {ElTree} from "element-plus";
|
||||
|
||||
import {useStore} from "vuex";
|
||||
const store = useStore();
|
||||
|
||||
import {useExecutionsStore} from "../../../stores/executions";
|
||||
import {usePluginsStore} from "../../../stores/plugins";
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
import {apiUrl} from "override/utils/route";
|
||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||
|
||||
import CopyToClipboard from "../../layout/CopyToClipboard.vue";
|
||||
|
||||
import Editor from "../../inputs/Editor.vue";
|
||||
const editorValue = ref("");
|
||||
const debugCollapse = ref("");
|
||||
import VarValue from "../VarValue.vue";
|
||||
import SubFlowLink from "../../flows/SubFlowLink.vue";
|
||||
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
|
||||
import TextBoxSearchOutline from "vue-material-design-icons/TextBoxSearchOutline.vue";
|
||||
|
||||
const store = useStore();
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
const editorValue = ref<string>("");
|
||||
const debugCollapse = ref<string>("");
|
||||
const debugEditor = ref<InstanceType<typeof Editor>>();
|
||||
const debugExpression = ref("");
|
||||
const debugExpression = ref<string>("");
|
||||
|
||||
const computedDebugValue = computed(() => {
|
||||
const formatTask = (task) => {
|
||||
if (!task) return "";
|
||||
@@ -236,15 +240,6 @@
|
||||
});
|
||||
};
|
||||
|
||||
import VarValue from "../VarValue.vue";
|
||||
import SubFlowLink from "../../flows/SubFlowLink.vue";
|
||||
|
||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||
|
||||
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
|
||||
import TextBoxSearchOutline from "vue-material-design-icons/TextBoxSearchOutline.vue";
|
||||
import {usePluginsStore} from "../../../stores/plugins";
|
||||
|
||||
const cascader = ref<InstanceType<typeof ElTree> | null>(null);
|
||||
const scrollRight = () =>
|
||||
setTimeout(
|
||||
@@ -431,133 +426,131 @@
|
||||
const leftWidth = ref("70%");
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.outputs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-splitter-bar {
|
||||
width: 3px !important;
|
||||
background-color: var(--ks-border-primary);
|
||||
:deep(.el-splitter-bar) {
|
||||
width: 3px !important;
|
||||
background-color: var(--ks-border-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ks-border-active);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ks-border-active);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar.el-cascader-menu:nth-of-type(-n + 2) ul li:first-child),
|
||||
.values {
|
||||
pointer-events: none;
|
||||
margin: 0.75rem 0 1.25rem 0;
|
||||
}
|
||||
|
||||
:deep(.el-cascader-menu__list) {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
:deep(.el-cascader-panel) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.debug {
|
||||
background: var(--ks-background-body);
|
||||
}
|
||||
|
||||
.bordered {
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
.bordered > :deep(.el-collapse-item) {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: var(--ks-background-card);
|
||||
}
|
||||
|
||||
:deep(.el-cascader-menu) {
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
.el-scrollbar.el-cascader-menu:nth-of-type(-n + 2) ul li:first-child,
|
||||
.values {
|
||||
pointer-events: none;
|
||||
margin: 0.75rem 0 1.25rem 0;
|
||||
}
|
||||
|
||||
.el-cascader-menu__list {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.el-cascader-panel {
|
||||
.el-cascader-menu__wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.debug {
|
||||
background: var(--ks-background-body);
|
||||
}
|
||||
& .el-cascader-node {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: var(--el-font-size-small);
|
||||
color: var(--ks-content-primary);
|
||||
|
||||
.bordered {
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
}
|
||||
|
||||
.bordered > .el-collapse-item {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
background: var(--ks-background-card);
|
||||
}
|
||||
|
||||
.el-cascader-menu {
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid var(--ks-border-primary);
|
||||
&[aria-haspopup="false"] {
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.el-cascader-menu__wrap {
|
||||
height: 100%;
|
||||
&:hover {
|
||||
background-color: var(--ks-border-primary);
|
||||
}
|
||||
|
||||
& .el-cascader-node {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: var(--el-font-size-small);
|
||||
&.in-active-path,
|
||||
&.is-active {
|
||||
background-color: var(--ks-border-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.el-cascader-node__prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task .wrapper {
|
||||
align-self: center;
|
||||
height: var(--el-font-size-small);
|
||||
width: var(--el-font-size-small);
|
||||
}
|
||||
|
||||
code span.regular {
|
||||
color: var(--ks-content-primary);
|
||||
|
||||
&[aria-haspopup="false"] {
|
||||
padding-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ks-border-primary);
|
||||
}
|
||||
|
||||
&.in-active-path,
|
||||
&.is-active {
|
||||
background-color: var(--ks-border-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.el-cascader-node__prefix {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task .wrapper {
|
||||
align-self: center;
|
||||
height: var(--el-font-size-small);
|
||||
width: var(--el-font-size-small);
|
||||
}
|
||||
|
||||
code span.regular {
|
||||
color: var(--ks-content-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.content-container {
|
||||
height: calc(100vh - 0px);
|
||||
.content-container {
|
||||
height: calc(100vh - 0px);
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
.el-collapse-item__wrap {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
.el-collapse-item__wrap {
|
||||
overflow-y: auto !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-collapse-item__content {
|
||||
overflow-y: auto !important;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.var-value) {
|
||||
.el-collapse-item__content {
|
||||
overflow-y: auto !important;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
:deep(.var-value) {
|
||||
overflow-y: auto !important;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
</style>
|
||||
@@ -35,7 +35,7 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import CopyToClipboard from "../layout/CopyToClipboard.vue";
|
||||
import Editor from "../inputs/Editor.vue";
|
||||
import {apiUrlWithoutTenants} from "../../override/utils/route";
|
||||
import {baseUrl, basePathWithoutTenant, apiUrlWithoutTenants} from "../../override/utils/route";
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
|
||||
interface Flow {
|
||||
@@ -73,7 +73,8 @@
|
||||
});
|
||||
|
||||
const generateWebhookUrl = (trigger: Trigger): string => {
|
||||
return `${apiUrlWithoutTenants()}/executions/webhook/${props.flow.namespace}/${props.flow.id}/${trigger.key}`;
|
||||
const origin = baseUrl ? apiUrlWithoutTenants() : `${location.origin}${basePathWithoutTenant()}`;
|
||||
return `${origin}/executions/webhook/${props.flow.namespace}/${props.flow.id}/${trigger.key}`;
|
||||
};
|
||||
|
||||
const generateWebhookCurlCommand = (trigger: Trigger): string => {
|
||||
|
||||
@@ -14,10 +14,11 @@ const createBaseUrl = (): string => {
|
||||
|
||||
export const baseUrl = createBaseUrl().replace(/\/$/, "")
|
||||
export const basePath = () => "/api/v1/main"
|
||||
export const basePathWithoutTenant = () => "/api/v1"
|
||||
|
||||
export const apiUrl = (_: Store<any>): string => {
|
||||
return `${baseUrl}${basePath()}`;
|
||||
}
|
||||
|
||||
export const apiUrlWithTenant = (store: Store<any>, _: RouteLocationNormalizedLoaded): string => apiUrl(store);
|
||||
export const apiUrlWithoutTenants = (): string => `${baseUrl}/api/v1`
|
||||
export const apiUrlWithoutTenants = (): string => `${baseUrl}${basePathWithoutTenant()}`;
|
||||
|
||||
@@ -47,7 +47,7 @@ import io.kestra.webserver.services.ExecutionDependenciesStreamingService;
|
||||
import io.kestra.webserver.services.ExecutionStreamingService;
|
||||
import io.kestra.webserver.utils.PageableUtils;
|
||||
import io.kestra.webserver.utils.RequestUtils;
|
||||
import io.kestra.webserver.utils.filepreview.FileRender;
|
||||
import io.kestra.plugin.core.preview.FileRender;
|
||||
import io.kestra.webserver.utils.filepreview.FileRenderBuilder;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.context.annotation.Value;
|
||||
@@ -201,6 +201,9 @@ public class ExecutionController {
|
||||
@Inject
|
||||
private LocalPathFactory localPathFactory;
|
||||
|
||||
@Inject
|
||||
private FileRenderBuilder fileRenderBuilder;
|
||||
|
||||
@Value("${" + LocalPath.ENABLE_PREVIEW_CONFIG + ":true}")
|
||||
private boolean enableLocalFilePreview;
|
||||
|
||||
@@ -1869,7 +1872,7 @@ public class ExecutionController {
|
||||
};
|
||||
|
||||
try (fileStream) {
|
||||
FileRender fileRender = FileRenderBuilder.of(
|
||||
FileRender fileRender = fileRenderBuilder.of(
|
||||
extension,
|
||||
fileStream,
|
||||
charset,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
|
||||
import io.kestra.plugin.core.preview.FileRender;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
|
||||
import io.kestra.plugin.core.preview.FileRender;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
|
||||
@@ -1,15 +1,40 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
|
||||
import io.kestra.plugin.core.preview.FileRender;
|
||||
import io.kestra.plugin.core.preview.PreviewRenderer;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererFactory;
|
||||
import io.kestra.plugin.core.preview.PreviewRendererRegistry;
|
||||
import io.kestra.plugin.core.preview.PreviewResult;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class FileRenderBuilder {
|
||||
private static final Charset DEFAULT_FILE_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
public static FileRender of(String extension, InputStream filestream, Optional<Charset> charset, Integer maxLine) throws IOException {
|
||||
@Inject
|
||||
private PreviewRendererFactory previewRendererFactory;
|
||||
|
||||
public FileRender of(String extension, InputStream filestream, Optional<Charset> charset, Integer maxLine) throws IOException {
|
||||
// we check plugin renderers first
|
||||
Optional<PreviewRenderer> pluginRenderer = previewRendererFactory.getRenderer(extension);
|
||||
if (pluginRenderer.isPresent()) {
|
||||
try {
|
||||
PreviewResult result = pluginRenderer.get().render(extension, filestream, charset, maxLine);
|
||||
return convertToFileRender(result);
|
||||
} catch (Exception e) {
|
||||
log.warn("Plugin preview renderer failed for extension '{}': {}", extension, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (ImageFileRender.ImageFileExtension.isImageFileExtension(extension)) {
|
||||
return new ImageFileRender(extension, filestream, maxLine);
|
||||
}
|
||||
@@ -21,4 +46,24 @@ public class FileRenderBuilder {
|
||||
default -> new DefaultFileRender(extension, filestream, charset.orElse(DEFAULT_FILE_CHARSET), maxLine);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private FileRender convertToFileRender(PreviewResult result) {
|
||||
return new FileRender(result.getExtension(), result.getMaxLines()) {
|
||||
{
|
||||
this.type = convertType(result.getType());
|
||||
this.content = result.getContent();
|
||||
this.truncated = result.isTruncated();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private FileRender.Type convertType(PreviewResult.Type type) {
|
||||
return switch (type) {
|
||||
case TEXT -> FileRender.Type.TEXT;
|
||||
case LIST -> FileRender.Type.LIST;
|
||||
case IMAGE -> FileRender.Type.IMAGE;
|
||||
case MARKDOWN -> FileRender.Type.MARKDOWN;
|
||||
case PDF -> FileRender.Type.PDF;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
|
||||
import io.kestra.core.serializers.FileSerde;
|
||||
import io.kestra.plugin.core.preview.FileRender;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.kestra.webserver.utils.filepreview;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
@@ -13,13 +14,16 @@ import java.util.stream.Stream;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class FileRenderBuilderTest {
|
||||
|
||||
@Inject
|
||||
private FileRenderBuilder fileRenderBuilder;
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideExtensions")
|
||||
void of(String extension, Class returnedClass) throws IOException {
|
||||
var emptyInput = new ByteArrayInputStream("".getBytes());
|
||||
var charset = StandardCharsets.UTF_8;
|
||||
|
||||
assertThat(FileRenderBuilder.of(extension, emptyInput, Optional.of(charset), 1000).getClass()).isEqualTo(returnedClass);
|
||||
assertThat(fileRenderBuilder.of(extension, emptyInput, Optional.of(charset), 1000).getClass()).isEqualTo(returnedClass);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> provideExtensions() {
|
||||
|
||||
Reference in New Issue
Block a user