Compare commits

...

5 Commits

22 changed files with 466 additions and 146 deletions

View File

@@ -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 }}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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 => {

View File

@@ -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()}`;

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,5 +1,6 @@
package io.kestra.webserver.utils.filepreview;
import io.kestra.plugin.core.preview.FileRender;
import lombok.Getter;
import java.io.BufferedReader;

View File

@@ -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;
};
}
}

View File

@@ -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;

View File

@@ -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() {