Compare commits

...

20 Commits

Author SHA1 Message Date
YannC
59c7d6a567 chore: upgrade to version 'v0.17.2' 2024-06-10 16:24:37 +02:00
brian.mulier
9e4e5f891e fix(ui): namespace files calls were not including tenant 2024-06-10 16:23:42 +02:00
Milos Paunovic
ea3ba991d1 fix(ui): amended output preview for sqs trigger messages for ion files 2024-06-10 16:23:11 +02:00
Miloš Paunović
1024c77289 chore(ui): showing ee tooltip only on click (#3951) 2024-06-10 16:23:04 +02:00
Miloš Paunović
36b29d6065 chore(ui): showing ee tooltip on hover only once, then, just on click (#3944) 2024-06-10 16:22:57 +02:00
YannC
1c8177e185 chore: upgrade to version 0.17.1 2024-06-05 22:38:53 +02:00
brian.mulier
3dd5d6bb71 fix(ui): prevent the need of loading all flows for Flow tab to be displayed in editor 2024-06-05 22:38:53 +02:00
YannC
16a641693a fix(ui): avoid 404 with autocomplete when flow does not exist 2024-06-05 22:21:07 +02:00
YannC
efdb075155 fix(core): Now accept an extension for the file input
close #3858
2024-06-05 22:21:02 +02:00
Miloš Paunović
a99d52a406 fix(ui): added safety checks for all tour related calls (#3938) 2024-06-05 22:20:53 +02:00
YannC
852edea36e fix(ui): dont count flow in tutorial namespace 2024-06-05 22:20:45 +02:00
brian.mulier
defa426259 fix(ui): null-safe guided tour access in TriggerFlow.vue 2024-06-05 22:20:38 +02:00
Miloš Paunović
3aadcfd683 fix(ui): flow default inputs are now properly populated (#3934) 2024-06-05 22:20:30 +02:00
YannC
0f5d59103a fix(core): remove @NotEmpty
close #3920
2024-06-05 22:20:16 +02:00
YannC
50b9120434 fix(core): UploadFiles now handle subfolders 2024-06-05 22:19:53 +02:00
Anna Geller
896c761502 feat: switch from contact-us to demo 2024-06-05 22:19:39 +02:00
Loïc Mathieu
381d1b381f chore: fix docker image build 2024-06-04 15:29:51 +02:00
Loïc Mathieu
72a428a439 core: add default 'true' to docker task 2024-06-04 14:45:57 +02:00
Loïc Mathieu
7447e61dbc chore: fix docker workflow variable computation 2024-06-04 14:45:52 +02:00
Loïc Mathieu
45ffc3cc22 fix: Maven description 2024-06-04 11:11:48 +02:00
23 changed files with 136 additions and 72 deletions

View File

@@ -7,14 +7,7 @@ on:
description: 'Retag latest Docker images'
required: true
type: string
options:
- "true"
- "false"
skip-test:
description: 'Skip test'
required: false
type: string
default: "false"
default: "true"
options:
- "true"
- "false"
@@ -125,6 +118,16 @@ jobs:
packages: python3 python3-venv python-is-python3 python3-pip nodejs npm curl zip unzip
python-libs: kestra
steps:
- uses: actions/checkout@v4
# Vars
- name: Set image name
id: vars
run: |
TAG=${GITHUB_REF#refs/*/}
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
# Download release
- name: Download release
uses: robinraju/release-downloader@v1.10
@@ -137,14 +140,6 @@ jobs:
run: |
cp build/executable/* docker/app/kestra && chmod +x docker/app/kestra
# Vars
- name: Set image name
id: vars
run: |
TAG=${GITHUB_REF#refs/*/}
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "plugins=${{ matrix.image.plugins }}" >> $GITHUB_OUTPUT
# Docker setup
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -179,7 +174,7 @@ jobs:
- name: Retag to latest
if: github.event.inputs.retag-latest == 'true'
run: |
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{1}', matrix.image.name) }}
regctl image copy ${{ format('kestra/kestra:{0}{1}', steps.vars.outputs.tag, matrix.image.name) }} ${{ format('kestra/kestra:latest{0}', matrix.image.name) }}
end:
runs-on: ubuntu-latest

View File

@@ -454,7 +454,7 @@ subprojects {
}
maven.pom {
description 'The modern, scalable orchestrator & scheduler open source platform'
description = 'The modern, scalable orchestrator & scheduler open source platform'
developers {
developer {

View File

@@ -1,18 +1,21 @@
package io.kestra.core.models.flows.input;
import io.kestra.core.models.flows.Input;
import jakarta.validation.ConstraintViolationException;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.net.URI;
import jakarta.validation.ConstraintViolationException;
@SuperBuilder
@Getter
@NoArgsConstructor
public class FileInput extends Input<URI> {
@Builder.Default
public String extension = ".upl";
@Override
public void validate(URI input) throws ConstraintViolationException {
// no validation yet

View File

@@ -8,6 +8,7 @@ import io.kestra.core.models.flows.Flow;
import io.kestra.core.models.flows.Input;
import io.kestra.core.models.flows.Type;
import io.kestra.core.models.flows.input.ArrayInput;
import io.kestra.core.models.flows.input.FileInput;
import io.kestra.core.models.tasks.common.EncryptedString;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.storages.StorageInterface;
@@ -85,7 +86,9 @@ public class FlowInputOutput {
.subscribeOn(Schedulers.boundedElastic())
.map(throwFunction(input -> {
if (input instanceof CompletedFileUpload fileUpload) {
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", ".upl");
String fileExtension = inputs.stream().filter(flowInput -> flowInput instanceof FileInput && flowInput.getId().equals(fileUpload.getFilename())).map(flowInput -> ((FileInput) flowInput).getExtension()).findFirst().orElse(".upl");
fileExtension = fileExtension.startsWith(".") ? fileExtension : "." + fileExtension;
File tempFile = File.createTempFile(fileUpload.getFilename() + "_", fileExtension);
try (var inputStream = fileUpload.getInputStream();
var outputStream = new FileOutputStream(tempFile)) {
long transferredBytes = inputStream.transferTo(outputStream);

View File

@@ -80,7 +80,6 @@ public class DeleteFiles extends Task implements RunnableTask<DeleteFiles.Output
private String namespace;
@NotNull
@NotEmpty
@Schema(
title = "A file or a list of files from the given namespace.",
description = "String or a list of strings; each string can either be a regex glob pattern or a file path URI.",

View File

@@ -11,7 +11,6 @@ import io.kestra.core.runners.RunContext;
import io.kestra.core.services.FlowService;
import io.kestra.core.utils.Rethrow;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -84,7 +83,6 @@ public class DownloadFiles extends Task implements RunnableTask<DownloadFiles.Ou
private String namespace;
@NotNull
@NotEmpty
@Schema(
title = "A file or a list of files from the given namespace.",
description = "String or a list of strings; each string can either be a regex glob pattern or a file path URI.",
@@ -93,11 +91,19 @@ public class DownloadFiles extends Task implements RunnableTask<DownloadFiles.Ou
@PluginProperty(dynamic = true)
private Object files;
@Schema(
title = "The folder where the downloaded files will be stored"
)
@PluginProperty(dynamic = true)
@Builder.Default
private String destination = "";
@Override
public Output run(RunContext runContext) throws Exception {
Logger logger = runContext.logger();
String renderedNamespace = runContext.render(namespace);
String renderedDestination = runContext.render(destination);
// Check if namespace is allowed
RunContext.FlowInfo flowInfo = runContext.flowInfo();
FlowService flowService = runContext.getApplicationContext().getBean(FlowService.class);
@@ -120,7 +126,7 @@ public class DownloadFiles extends Task implements RunnableTask<DownloadFiles.Ou
namespaceFilesService.recursiveList(flowInfo.tenantId(), renderedNamespace, null).forEach(Rethrow.throwConsumer(uri -> {
if (patterns.stream().anyMatch(p -> p.matches(Path.of(uri.getPath())))) {
try (InputStream inputStream = namespaceFilesService.content(flowInfo.tenantId(), renderedNamespace, uri)) {
downloaded.put(uri.getPath(), runContext.storage().putFile(inputStream, uri.getPath()));
downloaded.put(uri.getPath(), runContext.storage().putFile(inputStream, destination + uri.getPath()));
logger.debug(String.format("Downloaded %s", uri));
}
}

View File

@@ -138,10 +138,10 @@ public class UploadFiles extends Task implements RunnableTask<UploadFiles.Output
});
// check for file in current tempDir that match regexs
List<PathMatcher> patterns = regexs.stream().map(reg -> FileSystems.getDefault().getPathMatcher("glob:" + reg)).toList();
for (File file : Objects.requireNonNull(runContext.tempDir().toFile().listFiles())) {
List<PathMatcher> patterns = regexs.stream().map(reg -> FileSystems.getDefault().getPathMatcher("glob:" + runContext.tempDir().toString() + checkLeadingSlash(reg))).toList();
for (File file : Objects.requireNonNull(listFilesRecursively(runContext.tempDir().toFile()))) {
if (patterns.stream().anyMatch(p -> p.matches(Path.of(file.toURI().getPath())))) {
String newFilePath = buildPath(renderedDestination, file.getName());
String newFilePath = buildPath(renderedDestination, file.getPath().replace(runContext.tempDir().toString(), ""));
storeNewFile(logger, runContext, storageInterface, flowInfo.tenantId(), newFilePath, new FileInputStream(file));
}
}
@@ -199,6 +199,24 @@ public class UploadFiles extends Task implements RunnableTask<UploadFiles.Output
}
}
private List<File> listFilesRecursively(File directory) throws IOException {
List<File> files = new ArrayList<>();
if (directory == null || !directory.isDirectory()) {
return files; // Handle invalid directory or not a directory
}
for (File file : directory.listFiles()) {
if (file.isFile()) {
files.add(file);
} else {
// Recursively call for subdirectories
files.addAll(listFilesRecursively(file));
}
}
return files;
}
@Builder
@Getter
public static class Output implements io.kestra.core.models.tasks.Output {

View File

@@ -1,4 +1,4 @@
version=0.17.0
version=0.17.2
jacksonVersion=2.16.2
micronautVersion=4.4.3

View File

@@ -1,6 +1,10 @@
<template>
<el-tooltip :persistent="false" :focus-on-show="true" popper-class="ee-tooltip" :disabled="!disabled" :placement="placement">
<el-tooltip :visible="visible" :persistent="false" :focus-on-show="true" popper-class="ee-tooltip" :disabled="!disabled" :placement="placement">
<template #content v-if="link">
<el-button circle class="ee-tooltip-close" @click="changeVisibility(false)">
<Close />
</el-button>
<p>{{ $t("ee-tooltip.features-blocked") }}</p>
<a
@@ -13,7 +17,7 @@
</a>
</template>
<template #default>
<span ref="slot-container">
<span ref="slot-container" class="cursor-pointer" @click="changeVisibility()">
<slot />
<lock v-if="disabled" />
</span>
@@ -22,10 +26,11 @@
</template>
<script>
import Close from "vue-material-design-icons/Close.vue";
import Lock from "vue-material-design-icons/Lock.vue";
export default {
components: {Lock},
components: {Close, Lock},
props: {
top: {
type: Boolean,
@@ -48,6 +53,16 @@
default: undefined
},
},
data() {
return {
visible: false,
}
},
methods: {
changeVisibility(visible = true) {
this.visible = visible
}
},
computed: {
link() {
@@ -83,5 +98,13 @@
:deep(.material-design-icon) > .material-design-icon__svg {
bottom: -0.125em;
}
.ee-tooltip-close {
position: absolute;
top: 0;
right: 0;
border: none;
margin: 0.5rem;
}
</style>

View File

@@ -41,7 +41,7 @@
if (this.$route.query.reset) {
localStorage.setItem("tourDoneOrSkip", undefined);
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
this.$tours["guidedTour"].start();
this.$tours["guidedTour"]?.start();
}
this.setupFlow()
},

View File

@@ -67,7 +67,7 @@
},
methods: {
stopTour() {
this.$tours["guidedTour"].stop();
this.$tours["guidedTour"]?.stop();
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
},
},
@@ -79,7 +79,7 @@
if (!this.guidedProperties.tourStarted
&& localStorage.getItem("tourDoneOrSkip") !== "true"
&& this.total === 0) {
this.$tours["guidedTour"].start();
this.$tours["guidedTour"]?.start();
}
}, 200)
window.addEventListener("popstate", () => {

View File

@@ -55,7 +55,7 @@
handler: function (newValue) {
if (newValue?.manuallyContinue) {
setTimeout(() => {
this.$tours["guidedTour"].nextStep();
this.$tours["guidedTour"]?.nextStep();
this.$store.commit("core/setGuidedProperties", {manuallyContinue: false});
}, 500);
}

View File

@@ -91,12 +91,12 @@
},
methods: {
onClick() {
if (this.$tours["guidedTour"].isRunning.value) {
this.$tours["guidedTour"].nextStep();
if (this.$tours["guidedTour"]?.isRunning?.value) {
this.$tours["guidedTour"]?.nextStep();
this.$store.dispatch("api/events", {
type: "ONBOARDING",
onboarding: {
step: this.$tours["guidedTour"].currentStep._value,
step: this.$tours["guidedTour"]?.currentStep?._value,
action: "next",
template: this.guidedProperties.template
},
@@ -131,7 +131,7 @@
},
beforeClose(done){
if(this.guidedProperties.tourStarted) return;
this.reset();
done()
}

View File

@@ -385,7 +385,7 @@
},
computed: {
...mapState({
flows: (state) => state.flow.flows,
flow: (state) => state.flow.flow,
explorerVisible: (state) => state.editor.explorerVisible,
}),
folders() {
@@ -765,17 +765,17 @@
(function pushItemToFolder(basePath = "", array) {
for (const item of array) {
const folderPath = `${basePath}${item.fileName}`;
if (folderPath === SELF.dialog.folder && Array.isArray(item.children)) {
item.children = SELF.sorted([...item.children, NEW]);
return true; // Return true if the folder is found and item is pushed
}
if (Array.isArray(item.children) && pushItemToFolder(`${folderPath}/`, item.children)) {
return true; // Return true if the folder is found and item is pushed in recursive call
}
}
return false;
})(undefined, this.items);
}
@@ -883,9 +883,9 @@
},
},
watch: {
flows: {
flow: {
handler(flow) {
if (flow && flow.length) {
if (flow) {
this.changeOpenedTabs({
action: "open",
name: "Flow",
@@ -948,21 +948,21 @@
.empty {
position: relative;
top: 100px;
text-align: center;
text-align: center;
color: white;
html.light & {
color: $tertiary;
}
& img {
margin-bottom: 2rem;
margin-bottom: 2rem;
}
& h3 {
font-size: var(--font-size-lg);
font-weight: 500;
margin-bottom: .5rem;
font-weight: 500;
margin-bottom: .5rem;
}
& p {

View File

@@ -430,7 +430,8 @@
id: subflowTask.flowId,
revision: subflowTask.revision,
source: false,
store: false
store: false,
deleted: true
}
)).inputs?.map(input => input.id) ?? [];
} catch (e) {

View File

@@ -69,7 +69,7 @@
<Slack class="align-middle" /> {{ $t("join community") }}
</a>
<a
href="https://kestra.io/contact-us?utm_source=app&utm_content=top-nav-bar"
href="https://kestra.io/demo?utm_source=app&utm_content=top-nav-bar"
target="_blank"
class="d-flex gap-2 el-dropdown-menu__item"
>
@@ -147,7 +147,7 @@
localStorage.setItem("tourDoneOrSkip", undefined);
this.$store.commit("core/setGuidedProperties", {tourStarted: false});
this.$tours["guidedTour"].start();
this.$tours["guidedTour"]?.start();
}
}
};

View File

@@ -183,17 +183,17 @@ export default {
}
},
save() {
if (this.$tours["guidedTour"].isRunning.value && !this.guidedProperties.saveFlow) {
if (this.$tours["guidedTour"]?.isRunning?.value && !this.guidedProperties.saveFlow) {
this.$store.dispatch("api/events", {
type: "ONBOARDING",
onboarding: {
step: this.$tours["guidedTour"].currentStep._value,
step: this.$tours["guidedTour"]?.currentStep?._value,
action: "next",
template: this.guidedProperties.template
},
page: pageFromRoute(this.$router.currentRoute.value)
});
this.$tours["guidedTour"].nextStep();
this.$tours["guidedTour"]?.nextStep();
return;
}

View File

@@ -205,7 +205,19 @@ export default {
return this.$http.get(`${apiUrl(this)}/executions/${options.executionId}/file/preview`, {
params: options
}).then(response => {
commit("setFilePreview", response.data)
let data = {...response.data}
// WORKAROUND, related to https://github.com/kestra-io/plugin-aws/issues/456
if(data.extension === "ion") {
const notObjects = data.content.some(e => typeof e !== "object");
if(notObjects) {
const content = data.content.length === 1 ? data.content[0] : data.content.join("\n");
data = {...data, type: "TEXT", content}
}
}
commit("setFilePreview", data)
})
},
setLabels(_, options) {

View File

@@ -37,7 +37,7 @@ export default {
}).then(response => {
commit("setFlows", response.data.results)
commit("setTotal", response.data.total)
commit("setOverallTotal", response.data.total)
commit("setOverallTotal", response.data.results.filter(f => f.namespace !== "tutorial").length)
return response.data;
})

View File

@@ -1,7 +1,9 @@
import Utils from "../utils/utils";
import {apiUrl} from "override/utils/route";
const BASE = (namespace) => `${apiUrl(this)}/namespaces/${namespace}`;
function base(namespace) {
return `${apiUrl(this)}/namespaces/${namespace}`;
}
const HEADERS = {headers: {"Content-Type": "multipart/form-data"}};
const slashPrefix = (path) => (path.startsWith("/") ? path : `/${path}`);
@@ -15,13 +17,13 @@ export default {
actions: {
// Create a directory
async createDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files/directory?path=${slashPrefix(payload.path)}`;
const URL = `${base.call(this, payload.namespace)}/files/directory?path=${slashPrefix(payload.path)}`;
await this.$http.post(URL);
},
// List directory content
async readDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files/directory${payload.path ? `?path=${slashPrefix(safePath(payload.path))}` : ""}`;
const URL = `${base.call(this, payload.namespace)}/files/directory${payload.path ? `?path=${slashPrefix(safePath(payload.path))}` : ""}`;
const request = await this.$http.get(URL);
return request.data ?? [];
@@ -33,13 +35,13 @@ export default {
const BLOB = new Blob([payload.content], {type: "text/plain"});
DATA.append("fileContent", BLOB);
const URL = `${BASE(payload.namespace)}/files?path=${slashPrefix(payload.path)}`;
const URL = `${base.call(this, payload.namespace)}/files?path=${slashPrefix(payload.path)}`;
await this.$http.post(URL, DATA, HEADERS);
},
// Get namespace file content
async readFile(_, payload) {
const URL = `${BASE(payload.namespace)}/files?path=${slashPrefix(safePath(payload.path))}`;
const URL = `${base.call(this, payload.namespace)}/files?path=${slashPrefix(safePath(payload.path))}`;
const request = await this.$http.get(URL);
return request.data ?? [];
@@ -47,7 +49,7 @@ export default {
// Search for namespace files
async searchFiles(_, payload) {
const URL = `${BASE(payload.namespace)}/files/search?q=${payload.query}`;
const URL = `${base.call(this, payload.namespace)}/files/search?q=${payload.query}`;
const request = await this.$http.get(URL);
return request.data ?? [];
@@ -59,31 +61,31 @@ export default {
const BLOB = new Blob([payload.content], {type: "text/plain"});
DATA.append("fileContent", BLOB);
const URL = `${BASE(payload.namespace)}/files?path=${slashPrefix(safePath(payload.path))}`;
const URL = `${base.call(this, payload.namespace)}/files?path=${slashPrefix(safePath(payload.path))}`;
await this.$http.post(URL, DATA, HEADERS);
},
// Move a file or directory
async moveFileDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files?from=${slashPrefix(payload.old)}&to=${slashPrefix(payload.new)}`;
const URL = `${base.call(this, payload.namespace)}/files?from=${slashPrefix(payload.old)}&to=${slashPrefix(payload.new)}`;
await this.$http.put(URL);
},
// Rename a file or directory
async renameFileDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files?from=${slashPrefix(payload.old)}&to=${slashPrefix(payload.new)}`;
const URL = `${base.call(this, payload.namespace)}/files?from=${slashPrefix(payload.old)}&to=${slashPrefix(payload.new)}`;
await this.$http.put(URL);
},
// Delete a file or directory
async deleteFileDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files?path=${slashPrefix(payload.path)}`;
const URL = `${base.call(this, payload.namespace)}/files?path=${slashPrefix(payload.path)}`;
await this.$http.delete(URL);
},
// Export namespace files as a ZIP
async exportFileDirectory(_, payload) {
const URL = `${BASE(payload.namespace)}/files/export`;
const URL = `${base.call(this, payload.namespace)}/files/export`;
const request = await this.$http.get(URL);
const name = payload.namespace + "_files.zip";

View File

@@ -546,7 +546,7 @@
"environment color setting": "Environment color",
"slack support": "Ask any question via Slack",
"join community": "Join the Community",
"reach us": "Reach out to us",
"reach us": "Talk to us",
"new version": "New version {version} available!",
"error detected": "Error(s) detected",
"warning detected": "Warning(s) detected",

View File

@@ -14,6 +14,8 @@ export default class Inputs {
res = JSON.stringify(res).toString()
} else if (type === "BOOLEAN" && type === undefined){
res = "undefined";
} else if (type === "STRING" && Array.isArray(res)){
res = res.toString();
}
return res;
}

View File

@@ -84,7 +84,7 @@ export const executeTask = (submitor, flow, values, options) => {
}
}
if(options.nextStep) submitor.$tours["guidedTour"].nextStep();
if(options.nextStep) submitor.$tours["guidedTour"]?.nextStep();
return response.data;
})