Repository: kestra-io/kestra (java)

Issue:
Nested directories of Namespace Files lose their object prefix
### Explain the bug

Reproducer: start `docker run --rm -it -p 28080:8080  kestra/kestra:develop-full  server local`

Run:
```yaml
id: ci
namespace: prod

variables:
  host: http://host.docker.internal:28080/

tasks:
  - id: deploy
    type: io.kestra.core.tasks.flows.WorkingDirectory
    tasks:
      - id: clone
        type: io.kestra.plugin.git.Clone
        url: https://github.com/kestra-io/scripts
        branch: main

      - id: deploy_files
        type: io.kestra.plugin.scripts.shell.Commands
        warningOnStdErr: false
        runner: PROCESS
        commands:
          - /app/kestra namespace files update prod etl/data_lake/ --server={{vars.host}}
```

### Environment Information

- Kestra Version: develop full
- Operating System and Java Version (if not using Kestra Docker image):


Pull request:
fix/keep-file-structures-on-ns-files-update-cmd
this PR adds a "to" param to specify remote path for namespace files upload

closes #2480

Patch:
diff --git a/cli/src/main/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommand.java b/cli/src/main/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommand.java
index 890c1fcca..285134d75 100644
--- a/cli/src/main/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommand.java
+++ b/cli/src/main/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommand.java
@@ -1,7 +1,7 @@
 package io.kestra.cli.commands.namespaces.files;
 
+import io.kestra.cli.AbstractApiCommand;
 import io.kestra.cli.AbstractValidateCommand;
-import io.kestra.cli.commands.AbstractServiceNamespaceUpdateCommand;
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.MediaType;
 import io.micronaut.http.client.exceptions.HttpClientResponseException;
@@ -23,20 +23,33 @@ import java.util.List;
     mixinStandardHelpOptions = true
 )
 @Slf4j
-public class NamespaceFilesUpdateCommand extends AbstractServiceNamespaceUpdateCommand {
+public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
+    @CommandLine.Parameters(index = "0", description = "the namespace to update")
+    public String namespace;
+
+    @CommandLine.Parameters(index = "1", description = "the local directory containing files for current namespace")
+    public Path from;
+
+    @CommandLine.Parameters(index = "2", description = "the remote namespace path to upload files to", defaultValue = "/")
+    public String to;
+
+    @CommandLine.Option(names = {"--delete"}, negatable = true, description = "if missing should be deleted")
+    public boolean delete = false;
 
     private static final String KESTRA_IGNORE_FILE = ".kestraignore";
 
     @Override
     public Integer call() throws Exception {
         super.call();
+        to = to.startsWith("/") ? to : "/" + to;
+        to = to.endsWith("/") ? to : to + "/";
 
-        try (var files = Files.walk(directory); DefaultHttpClient client = client()) {
+        try (var files = Files.walk(from); DefaultHttpClient client = client()) {
             if (delete) {
-                client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/") + namespace + "/files?path=/", null)));
+                client.toBlocking().exchange(this.requestOptions(HttpRequest.DELETE(apiUri("/namespaces/") + namespace + "/files?path=" + to, null)));
             }
 
-            GitIgnore gitIgnore = parseKestraIgnore(directory);
+            GitIgnore gitIgnore = parseKestraIgnore(from);
 
             List<Path> paths = files
                 .filter(Files::isRegularFile)
@@ -47,16 +60,17 @@ public class NamespaceFilesUpdateCommand extends AbstractServiceNamespaceUpdateC
                 MultipartBody body = MultipartBody.builder()
                     .addPart("fileContent", path.toFile())
                     .build();
-                Path dest = directory.relativize(path);
+                String relativizedPath = from.relativize(path).toString();
+                String destination = to + relativizedPath;
                 client.toBlocking().exchange(
                     this.requestOptions(
                         HttpRequest.POST(
-                            apiUri("/namespaces/") + namespace + "/files?path=/" + dest,
+                            apiUri("/namespaces/") + namespace + "/files?path=" + destination,
                             body
                         ).contentType(MediaType.MULTIPART_FORM_DATA)
                     )
                 );
-                stdOut("Successfully uploaded {0} to /{1}", path, dest);
+                stdOut("Successfully uploaded {0} to {1}", path.toString(), destination);
             });
         } catch (HttpClientResponseException e) {
             AbstractValidateCommand.handleHttpException(e, "namespace");
diff --git a/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java b/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
index c1ea773c2..a19bf27cc 100644
--- a/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
+++ b/jdbc/src/main/java/io/kestra/jdbc/runner/JdbcExecutor.java
@@ -390,7 +390,7 @@ public class JdbcExecutor implements ExecutorInterface {
                         String log = "Create new execution for flow '" +
                             workerTaskExecution.getExecution()
                                 .getNamespace() + "'.'" + workerTaskExecution.getExecution().getFlowId() +
-                            "' with id '" + workerTaskExecution.getExecution() + "'";
+                            "' with id '" + workerTaskExecution.getExecution().getId() + "'";
 
                         JdbcExecutor.log.info(log);
 
diff --git a/ui/src/components/executions/Executions.vue b/ui/src/components/executions/Executions.vue
index d3c451ea4..ab761d829 100644
--- a/ui/src/components/executions/Executions.vue
+++ b/ui/src/components/executions/Executions.vue
@@ -330,47 +330,58 @@
                 optionalColumns: [
                     {
                         label: "start date",
-                        prop: "state.startDate"
+                        prop: "state.startDate",
+                        default: true
                     },
                     {
                         label: "end date",
-                        prop: "state.endDate"
+                        prop: "state.endDate",
+                        default: true
                     },
                     {
                         label: "duration",
-                        prop: "state.duration"
+                        prop: "state.duration",
+                        default: true
                     },
                     {
                         label: "state",
-                        prop: "state.current"
+                        prop: "state.current",
+                        default: true
                     },
                     {
                         label: "triggers",
-                        prop: "triggers"
+                        prop: "triggers",
+                        default: true
                     },
                     {
                         label: "labels",
-                        prop: "labels"
+                        prop: "labels",
+                        default: true
                     },
                     {
                         label: "inputs",
-                        prop: "inputs"
+                        prop: "inputs",
+                        default: false
                     },
                     {
                         label: "namespace",
-                        prop: "namespace"
+                        prop: "namespace",
+                        default: true
                     },
                     {
                         label: "flow",
-                        prop: "flowId"
+                        prop: "flowId",
+                        default: true
                     },
                     {
                         label: "revision",
-                        prop: "flowRevision"
+                        prop: "flowRevision",
+                        default: false
                     },
                     {
                         label: "task id",
-                        prop: "taskRunList.taskId"
+                        prop: "taskRunList.taskId",
+                        default: false
                     }
                 ],
                 displayColumns: [],
@@ -384,7 +395,7 @@
                 this.optionalColumns = this.optionalColumns.filter(col => col.prop !== "namespace" && col.prop !== "flowId")
             }
             this.displayColumns = localStorage.getItem(this.storageKey)?.split(",")
-                || this.optionalColumns.map(col => col.prop);
+                || this.optionalColumns.filter(col => col.default).map(col => col.prop);
         },
         computed: {
             ...mapState("execution", ["executions", "total"]),


Test patch:
diff --git a/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommandTest.java b/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommandTest.java
index be72eb19d..3ffb97fd5 100644
--- a/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommandTest.java
+++ b/cli/src/test/java/io/kestra/cli/commands/namespaces/files/NamespaceFilesUpdateCommandTest.java
@@ -4,10 +4,13 @@ import io.micronaut.configuration.picocli.PicocliRunner;
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.context.env.Environment;
 import io.micronaut.runtime.server.EmbeddedServer;
+import org.hamcrest.Matcher;
 import org.junit.jupiter.api.Test;
 
+import javax.annotation.Nullable;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
+import java.net.URISyntaxException;
 import java.net.URL;
 
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -16,7 +19,7 @@ import static org.hamcrest.core.StringContains.containsString;
 
 class NamespaceFilesUpdateCommandTest {
     @Test
-    void runWithoutIgnore() {
+    void runWithToSpecified() {
         URL directory = NamespaceFilesUpdateCommandTest.class.getClassLoader().getResource("namespacefiles/noignore");
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
@@ -26,6 +29,7 @@ class NamespaceFilesUpdateCommandTest {
             EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
             embeddedServer.start();
 
+            String to = "/some/directory";
             String[] args = {
                 "--server",
                 embeddedServer.getURL().toString(),
@@ -34,19 +38,49 @@ class NamespaceFilesUpdateCommandTest {
                 "--delete",
                 "io.kestra.cli",
                 directory.getPath(),
+                to
             };
             PicocliRunner.call(NamespaceFilesUpdateCommand.class, ctx, args);
 
-            assertThat(out.toString(), containsString("namespacefiles/noignore/2 to /2"));
-            assertThat(out.toString(), containsString("namespacefiles/noignore/flows/flow.yml"));
-            assertThat(out.toString(), containsString("namespacefiles/noignore/1 to /1"));
+            assertTransferMessage(out, "2", to);
+            assertTransferMessage(out, "1", to);
+            assertTransferMessage(out, "flows/flow.yml", to);
             out.reset();
         }
     }
 
     @Test
-    void runWithIgnore() {
-        URL directory = NamespaceFilesUpdateCommandTest.class.getClassLoader().getResource("namespacefiles/ignore");
+    void runWithoutIgnore() throws URISyntaxException {
+        URL directory = NamespaceFilesUpdateCommandTest.class.getClassLoader().getResource("namespacefiles/noignore/");
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        System.setOut(new PrintStream(out));
+
+        try (ApplicationContext ctx = ApplicationContext.run(Environment.CLI, Environment.TEST)) {
+
+            EmbeddedServer embeddedServer = ctx.getBean(EmbeddedServer.class);
+            embeddedServer.start();
+
+            String[] args = {
+                "--server",
+                embeddedServer.getURL().toString(),
+                "--user",
+                "myuser:pass:word",
+                "--delete",
+                "io.kestra.cli",
+                directory.getPath()
+            };
+            PicocliRunner.call(NamespaceFilesUpdateCommand.class, ctx, args);
+
+            assertTransferMessage(out, "2", null);
+            assertTransferMessage(out, "1", null);
+            assertTransferMessage(out, "flows/flow.yml", null);
+            out.reset();
+        }
+    }
+
+    @Test
+    void runWithIgnore() throws URISyntaxException {
+        URL directory = NamespaceFilesUpdateCommandTest.class.getClassLoader().getResource("namespacefiles/ignore/");
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         System.setOut(new PrintStream(out));
 
@@ -66,10 +100,20 @@ class NamespaceFilesUpdateCommandTest {
             };
             PicocliRunner.call(NamespaceFilesUpdateCommand.class, ctx, args);
 
-            assertThat(out.toString(), containsString("namespacefiles/ignore/2 to /2"));
-            assertThat(out.toString(), containsString("namespacefiles/ignore/1 to /1"));
-            assertThat(out.toString(), not(containsString("namespacefiles/ignore/flows/flow.yml")));
+            assertTransferMessage(out, "2", null);
+            assertTransferMessage(out, "1", null);
+            assertTransferMessage(out, "flows/flow.yml", null, false);
             out.reset();
         }
     }
+
+    private void assertTransferMessage(ByteArrayOutputStream out, String from, String to) {
+        assertTransferMessage(out, from, to, true);
+    }
+
+    private void assertTransferMessage(ByteArrayOutputStream out, String relativePath, @Nullable String to, boolean present) {
+        to = to == null ? "" : to;
+        Matcher<String> matcher = containsString(relativePath + " to " + to + "/" + relativePath);
+        assertThat(out.toString(), present ? matcher : not(matcher));
+    }
 }
\ No newline at end of file