mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-29 09:00:26 -05:00
Compare commits
56 Commits
fix/preser
...
issue/1091
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b004e1e1 | ||
|
|
b74d09accb | ||
|
|
5be401d23c | ||
|
|
bb9f4be8c2 | ||
|
|
01e8e46b77 | ||
|
|
d00f4b0768 | ||
|
|
279f59c874 | ||
|
|
d897509726 | ||
|
|
0d592342af | ||
|
|
fc690bf7cd | ||
|
|
0a1b919863 | ||
|
|
2f4e981a29 | ||
|
|
5e7739432e | ||
|
|
8aba863b8c | ||
|
|
7eaa43c50f | ||
|
|
267ff78bfe | ||
|
|
7272cfe01f | ||
|
|
91e2fdb2cc | ||
|
|
a236688be6 | ||
|
|
81763d40ae | ||
|
|
677efb6739 | ||
|
|
b35924fef1 | ||
|
|
9dd93294b6 | ||
|
|
fac6dfe9a0 | ||
|
|
3bf9764505 | ||
|
|
c35cea5d19 | ||
|
|
4d8e9479f1 | ||
|
|
3f24e8e838 | ||
|
|
7175fcb666 | ||
|
|
2ddfa13b1b | ||
|
|
ba2a5dfec8 | ||
|
|
f84441dac7 | ||
|
|
433b788e4a | ||
|
|
65c5fd6331 | ||
|
|
421ab40276 | ||
|
|
efb2779693 | ||
|
|
74d371c0ca | ||
|
|
90a7869020 | ||
|
|
d9ccb50b0f | ||
|
|
aea0b87ef8 | ||
|
|
9a144fc3fe | ||
|
|
ddd9cebc63 | ||
|
|
1bebbb9b73 | ||
|
|
8de4dc867e | ||
|
|
fc49694e76 | ||
|
|
152300abae | ||
|
|
1ff5dda4e1 | ||
|
|
84f9b8876d | ||
|
|
575955567f | ||
|
|
d6d2580b45 | ||
|
|
070e54b902 | ||
|
|
829ca4380f | ||
|
|
381c7a75ad | ||
|
|
1688c489a9 | ||
|
|
93ccbf5f9b | ||
|
|
ac1cb235e5 |
4
.github/workflows/auto-translate-ui-keys.yml
vendored
4
.github/workflows/auto-translate-ui-keys.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
|
||||
32
.github/workflows/kestra-devtools-test.yml
vendored
Normal file
32
.github/workflows/kestra-devtools-test.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: kestra-devtools test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- 'dev-tools/kestra-devtools/**'
|
||||
|
||||
env:
|
||||
# to save corepack from itself
|
||||
COREPACK_INTEGRITY_KEYS: 0
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: kestra-devtools tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Npm - install
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm run test
|
||||
|
||||
- name: Npm - Run build
|
||||
working-directory: 'dev-tools/kestra-devtools'
|
||||
run: npm run build
|
||||
4
.github/workflows/vulnerabilities-check.yml
vendored
4
.github/workflows/vulnerabilities-check.yml
vendored
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
|
||||
- name: Docker Vulnerabilities Check
|
||||
uses: aquasecurity/trivy-action@0.33.0
|
||||
uses: aquasecurity/trivy-action@0.33.1
|
||||
with:
|
||||
image-ref: kestra/kestra:develop
|
||||
format: 'template'
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
|
||||
# Run Trivy image scan for Docker vulnerabilities, see https://github.com/aquasecurity/trivy-action
|
||||
- name: Docker Vulnerabilities Check
|
||||
uses: aquasecurity/trivy-action@0.33.0
|
||||
uses: aquasecurity/trivy-action@0.33.1
|
||||
with:
|
||||
image-ref: kestra/kestra:latest
|
||||
format: table
|
||||
|
||||
9
.github/workflows/workflow-backend-test.yml
vendored
9
.github/workflows/workflow-backend-test.yml
vendored
@@ -20,6 +20,7 @@ permissions:
|
||||
contents: write
|
||||
checks: write
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -59,6 +60,14 @@ jobs:
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.gcp-service-account.json
|
||||
./gradlew check javadoc --parallel
|
||||
|
||||
- name: comment PR with test report
|
||||
if: always()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_AUTH_TOKEN }}
|
||||
run: |
|
||||
export KESTRA_PWD=$(pwd) && sh -c 'cd dev-tools/kestra-devtools && npm ci && npm run build && node dist/kestra-devtools-cli.cjs generateTestReportSummary --only-errors --ci $KESTRA_PWD' > report.md
|
||||
cat report.md
|
||||
|
||||
# report test
|
||||
- name: Test - Publish Test Results
|
||||
uses: dorny/test-reporter@v2
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
name: Pull Request - Delete Docker
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
||||
if: github.repository == 'kestra-io/kestra' # prevent running on forks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dataaxiom/ghcr-cleanup-action@v1
|
||||
|
||||
@@ -8,12 +8,12 @@ on:
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build Artifacts
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
||||
if: github.repository == 'kestra-io/kestra' # prevent running on forks
|
||||
uses: ./.github/workflows/workflow-build-artifacts.yml
|
||||
|
||||
publish:
|
||||
name: Publish Docker
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name # prevent running on forks
|
||||
if: github.repository == 'kestra-io/kestra' # prevent running on forks
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-artifacts
|
||||
env:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
# Add comment on pull request
|
||||
- name: Add comment to PR
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -32,7 +32,7 @@ plugins {
|
||||
|
||||
// release
|
||||
id 'net.researchgate.release' version '3.1.0'
|
||||
id "com.gorylenko.gradle-git-properties" version "2.5.2"
|
||||
id "com.gorylenko.gradle-git-properties" version "2.5.3"
|
||||
id 'signing'
|
||||
id "com.vanniktech.maven.publish" version "0.34.0"
|
||||
|
||||
@@ -207,6 +207,13 @@ subprojects {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
reports {
|
||||
junitXml.required = true
|
||||
junitXml.outputPerTestCase = true
|
||||
junitXml.mergeReruns = true
|
||||
junitXml.includeSystemErrLog = true;
|
||||
junitXml.outputLocation = layout.buildDirectory.dir("test-results/junit")
|
||||
}
|
||||
|
||||
// set Xmx for test workers
|
||||
maxHeapSize = '4g'
|
||||
|
||||
@@ -39,8 +39,6 @@ public class NamespaceFilesUpdateCommand extends AbstractApiCommand {
|
||||
@Inject
|
||||
private TenantIdSelectorService tenantService;
|
||||
|
||||
private static final String KESTRA_IGNORE_FILE = ".kestraignore";
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
super.call();
|
||||
|
||||
@@ -4,11 +4,15 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.http.HttpRequest;
|
||||
import io.kestra.core.http.HttpResponse;
|
||||
import io.kestra.core.http.client.apache.*;
|
||||
import io.kestra.core.http.client.apache.FailedResponseInterceptor;
|
||||
import io.kestra.core.http.client.apache.LoggingRequestInterceptor;
|
||||
import io.kestra.core.http.client.apache.LoggingResponseInterceptor;
|
||||
import io.kestra.core.http.client.apache.RunContextResponseInterceptor;
|
||||
import io.kestra.core.http.client.configurations.HttpConfiguration;
|
||||
import io.kestra.core.runners.DefaultRunContext;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.utils.RetryUtils;
|
||||
import io.micrometer.common.KeyValues;
|
||||
import io.micrometer.core.instrument.binder.httpcomponents.hc5.ApacheHttpClientContext;
|
||||
import io.micrometer.core.instrument.binder.httpcomponents.hc5.DefaultApacheHttpClientObservationConvention;
|
||||
@@ -21,7 +25,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.client5.http.ContextBuilder;
|
||||
import org.apache.hc.client5.http.auth.*;
|
||||
import org.apache.hc.client5.http.auth.AuthScope;
|
||||
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.impl.ChainElement;
|
||||
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
|
||||
@@ -39,6 +44,8 @@ import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.ssl.SSLContexts;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -46,11 +53,8 @@ import java.net.*;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
@Slf4j
|
||||
public class HttpClient implements Closeable {
|
||||
@@ -75,7 +79,7 @@ public class HttpClient implements Closeable {
|
||||
throw new IllegalStateException("Client has already been created");
|
||||
}
|
||||
|
||||
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom()
|
||||
var builder = HttpClients.custom()
|
||||
.disableDefaultUserAgent()
|
||||
.setUserAgent("Kestra");
|
||||
|
||||
@@ -89,49 +93,37 @@ public class HttpClient implements Closeable {
|
||||
// logger
|
||||
if (this.configuration.getLogs() != null && this.configuration.getLogs().length > 0) {
|
||||
if (ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.REQUEST_HEADERS) ||
|
||||
ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.REQUEST_BODY)
|
||||
) {
|
||||
ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.REQUEST_BODY)) {
|
||||
builder.addRequestInterceptorLast(new LoggingRequestInterceptor(runContext.logger(), this.configuration.getLogs()));
|
||||
}
|
||||
|
||||
if (ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.RESPONSE_HEADERS) ||
|
||||
ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.RESPONSE_BODY)
|
||||
) {
|
||||
ArrayUtils.contains(this.configuration.getLogs(), HttpConfiguration.LoggingType.RESPONSE_BODY)) {
|
||||
builder.addResponseInterceptorLast(new LoggingResponseInterceptor(runContext.logger(), this.configuration.getLogs()));
|
||||
}
|
||||
}
|
||||
|
||||
// Object dependencies
|
||||
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
|
||||
ConnectionConfig.Builder connectionConfig = ConnectionConfig.custom();
|
||||
BasicCredentialsProvider credentialsStore = new BasicCredentialsProvider();
|
||||
var connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
|
||||
var connectionConfig = ConnectionConfig.custom();
|
||||
var credentialsStore = new BasicCredentialsProvider();
|
||||
|
||||
// Timeout
|
||||
if (this.configuration.getTimeout() != null) {
|
||||
var connectTimeout = runContext.render(this.configuration.getTimeout().getConnectTimeout()).as(Duration.class);
|
||||
var connectTimeout = runContext.render(this.configuration.getTimeout().getConnectTimeout()).as(java.time.Duration.class);
|
||||
connectTimeout.ifPresent(duration -> connectionConfig.setConnectTimeout(Timeout.of(duration)));
|
||||
|
||||
var readIdleTimeout = runContext.render(this.configuration.getTimeout().getReadIdleTimeout()).as(Duration.class);
|
||||
var readIdleTimeout = runContext.render(this.configuration.getTimeout().getReadIdleTimeout()).as(java.time.Duration.class);
|
||||
readIdleTimeout.ifPresent(duration -> connectionConfig.setSocketTimeout(Timeout.of(duration)));
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (this.configuration.getProxy() != null && configuration.getProxy().getAddress() != null) {
|
||||
String proxyAddress = runContext.render(configuration.getProxy().getAddress()).as(String.class).orElse(null);
|
||||
var proxyAddress = runContext.render(configuration.getProxy().getAddress()).as(String.class).orElse(null);
|
||||
|
||||
if (StringUtils.isNotEmpty(proxyAddress)) {
|
||||
int port = runContext.render(configuration.getProxy().getPort()).as(Integer.class).orElseThrow();
|
||||
SocketAddress proxyAddr = new InetSocketAddress(
|
||||
proxyAddress,
|
||||
port
|
||||
);
|
||||
|
||||
Proxy proxy = new Proxy(runContext.render(configuration.getProxy().getType()).as(Proxy.Type.class).orElse(null), proxyAddr);
|
||||
var port = runContext.render(configuration.getProxy().getPort()).as(Integer.class).orElseThrow();
|
||||
var proxyAddr = new InetSocketAddress(proxyAddress, port);
|
||||
var proxy = new Proxy(runContext.render(configuration.getProxy().getType()).as(Proxy.Type.class).orElse(null), proxyAddr);
|
||||
|
||||
builder.setProxySelector(new ProxySelector() {
|
||||
@Override
|
||||
public void connectFailed(URI uri, SocketAddress sa, IOException e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,7 +134,6 @@ public class HttpClient implements Closeable {
|
||||
|
||||
if (this.configuration.getProxy().getUsername() != null && this.configuration.getProxy().getPassword() != null) {
|
||||
builder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy());
|
||||
|
||||
credentialsStore.setCredentials(
|
||||
new AuthScope(proxyAddress, port),
|
||||
new UsernamePasswordCredentials(
|
||||
@@ -154,19 +145,16 @@ public class HttpClient implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
// ssl
|
||||
if (this.configuration.getSsl() != null) {
|
||||
if (this.configuration.getSsl().getInsecureTrustAllCertificates() != null) {
|
||||
connectionManagerBuilder.setSSLSocketFactory(this.selfSignedConnectionSocketFactory());
|
||||
}
|
||||
}
|
||||
|
||||
// auth
|
||||
if (this.configuration.getAuth() != null) {
|
||||
this.configuration.getAuth().configure(builder, runContext);
|
||||
}
|
||||
|
||||
// root options
|
||||
if (!runContext.render(this.configuration.getFollowRedirects()).as(Boolean.class).orElseThrow()) {
|
||||
builder.disableRedirectHandling();
|
||||
}
|
||||
@@ -176,8 +164,7 @@ public class HttpClient implements Closeable {
|
||||
}
|
||||
|
||||
if (this.configuration.getAllowedResponseCodes() != null) {
|
||||
List<Integer> list = runContext.render(this.configuration.getAllowedResponseCodes()).asList(Integer.class);
|
||||
|
||||
var list = runContext.render(this.configuration.getAllowedResponseCodes()).asList(Integer.class);
|
||||
if (!list.isEmpty()) {
|
||||
builder.addResponseInterceptorLast(new FailedResponseInterceptor(list));
|
||||
}
|
||||
@@ -185,91 +172,51 @@ public class HttpClient implements Closeable {
|
||||
|
||||
builder.addResponseInterceptorLast(new RunContextResponseInterceptor(this.runContext));
|
||||
|
||||
// builder object
|
||||
connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig.build());
|
||||
builder.setConnectionManager(connectionManagerBuilder.build());
|
||||
builder.setDefaultCredentialsProvider(credentialsStore);
|
||||
|
||||
this.client = builder.build();
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private SSLConnectionSocketFactory selfSignedConnectionSocketFactory() {
|
||||
try {
|
||||
SSLContext sslContext = SSLContexts
|
||||
.custom()
|
||||
.loadTrustMaterial(null, (chain, authType) -> true)
|
||||
.build();
|
||||
|
||||
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).build();
|
||||
return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request
|
||||
*
|
||||
* @param request the request
|
||||
* @param cls the class of the response
|
||||
* @param <T> the type of response expected
|
||||
* @return the response
|
||||
*/
|
||||
public <T> HttpResponse<T> request(HttpRequest request, Class<T> cls) throws HttpClientException, IllegalVariableEvaluationException {
|
||||
HttpClientContext httpClientContext = this.clientContext(request);
|
||||
|
||||
var httpClientContext = this.clientContext();
|
||||
return this.request(request, httpClientContext, r -> {
|
||||
T body = bodyHandler(cls, r.getEntity());
|
||||
|
||||
return HttpResponse.from(r, body, request, httpClientContext);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request, getting the response with body as input stream
|
||||
*
|
||||
* @param request the request
|
||||
* @param consumer the consumer of the response
|
||||
* @return the response without the body
|
||||
*/
|
||||
public HttpResponse<Void> request(HttpRequest request, Consumer<HttpResponse<InputStream>> consumer) throws HttpClientException, IllegalVariableEvaluationException {
|
||||
HttpClientContext httpClientContext = this.clientContext(request);
|
||||
|
||||
var httpClientContext = this.clientContext();
|
||||
return this.request(request, httpClientContext, r -> {
|
||||
HttpResponse<InputStream> from = HttpResponse.from(
|
||||
r,
|
||||
r.getEntity() != null ? r.getEntity().getContent() : null,
|
||||
request,
|
||||
httpClientContext
|
||||
);
|
||||
|
||||
var from = HttpResponse.from(r, r.getEntity() != null ? r.getEntity().getContent() : null, request, httpClientContext);
|
||||
consumer.accept(from);
|
||||
|
||||
return HttpResponse.from(r, null, request, httpClientContext);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request and expect a json response
|
||||
*
|
||||
* @param request the request
|
||||
* @param <T> the type of response expected
|
||||
* @return the response
|
||||
*/
|
||||
public <T> HttpResponse<T> request(HttpRequest request) throws HttpClientException, IllegalVariableEvaluationException {
|
||||
HttpClientContext httpClientContext = this.clientContext(request);
|
||||
|
||||
var httpClientContext = this.clientContext();
|
||||
return this.request(request, httpClientContext, response -> {
|
||||
T body = JacksonMapper.ofJson().readValue(response.getEntity().getContent(), new TypeReference<>() {});
|
||||
|
||||
T body = JacksonMapper.ofJson().readValue(response.getEntity().getContent(), new TypeReference<>() {
|
||||
});
|
||||
return HttpResponse.from(response, body, request, httpClientContext);
|
||||
});
|
||||
}
|
||||
|
||||
private HttpClientContext clientContext(HttpRequest request) {
|
||||
ContextBuilder contextBuilder = ContextBuilder.create();
|
||||
|
||||
private HttpClientContext clientContext() {
|
||||
var contextBuilder = ContextBuilder.create();
|
||||
return contextBuilder.build();
|
||||
}
|
||||
|
||||
@@ -277,22 +224,31 @@ public class HttpClient implements Closeable {
|
||||
HttpRequest request,
|
||||
HttpClientContext httpClientContext,
|
||||
HttpClientResponseHandler<HttpResponse<T>> responseHandler
|
||||
) throws HttpClientException {
|
||||
try {
|
||||
return this.client.execute(request.to(runContext), httpClientContext, responseHandler);
|
||||
} catch (SocketException e) {
|
||||
throw new HttpClientRequestException(e.getMessage(), request, e);
|
||||
} catch (IOException e) {
|
||||
if (e instanceof SSLHandshakeException) {
|
||||
throw new HttpClientRequestException(e.getMessage(), request, e);
|
||||
}
|
||||
) throws HttpClientException, IllegalVariableEvaluationException {
|
||||
|
||||
if (e.getCause() instanceof HttpClientException httpClientException) {
|
||||
throw httpClientException;
|
||||
}
|
||||
var retryableCodes = runContext.render(configuration.getRetryOnStatusCodes()).asList(Integer.class);
|
||||
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return new RetryUtils().<HttpResponse<T>, HttpClientException>of(configuration.getRetry())
|
||||
.run(
|
||||
(res, throwable) -> {
|
||||
if (throwable instanceof HttpClientResponseException ex) {
|
||||
return retryableCodes.contains(ex.getResponse().getStatus().getCode());
|
||||
}
|
||||
return throwable instanceof HttpClientRequestException
|
||||
|| throwable instanceof SocketException
|
||||
|| throwable instanceof SSLHandshakeException;
|
||||
},
|
||||
() -> {
|
||||
try {
|
||||
return this.client.execute(request.to(runContext), httpClientContext, responseHandler);
|
||||
} catch (org.apache.hc.client5.http.ClientProtocolException e) {
|
||||
if (e.getCause() instanceof HttpClientException ex) {
|
||||
throw ex;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
@@ -2,6 +2,8 @@ package io.kestra.core.http.client.configurations;
|
||||
|
||||
import io.kestra.core.models.annotations.PluginProperty;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.tasks.retrys.AbstractRetry;
|
||||
import io.kestra.core.models.tasks.retrys.Exponential;
|
||||
import io.micronaut.logging.LogLevel;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
@@ -55,6 +57,19 @@ public class HttpConfiguration {
|
||||
@PluginProperty
|
||||
private LoggingType[] logs;
|
||||
|
||||
@Schema(title = "Retry strategy for HTTP requests.")
|
||||
@Builder.Default
|
||||
private AbstractRetry retry = Exponential.builder()
|
||||
.interval(Duration.ofMillis(1000))
|
||||
.maxInterval(Duration.ofSeconds(30))
|
||||
.maxAttempts(3)
|
||||
.build();
|
||||
|
||||
@Setter
|
||||
@Schema(title = "HTTP status codes that should be retried.")
|
||||
@Builder.Default
|
||||
private Property<List<Integer>> retryOnStatusCodes = Property.ofValue(List.of(502, 503, 504));
|
||||
|
||||
public enum LoggingType {
|
||||
REQUEST_HEADERS,
|
||||
REQUEST_BODY,
|
||||
@@ -62,7 +77,6 @@ public class HttpConfiguration {
|
||||
RESPONSE_BODY
|
||||
}
|
||||
|
||||
// Deprecated properties
|
||||
@Schema(title = "The time allowed to establish a connection to the server before failing.")
|
||||
@Deprecated
|
||||
private final Duration connectTimeout;
|
||||
@@ -104,7 +118,6 @@ public class HttpConfiguration {
|
||||
@Deprecated
|
||||
private final LogLevel logLevel;
|
||||
|
||||
// Deprecated properties with no equivalent value to be kept, silently ignore
|
||||
@Schema(title = "The time allowed for a read connection to remain idle before closing it.")
|
||||
@Deprecated
|
||||
private final Duration readIdleTimeout;
|
||||
@@ -121,115 +134,73 @@ public class HttpConfiguration {
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder connectTimeout(Duration connectTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
this.timeout = TimeoutConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.connectTimeout(Property.ofValue(connectTimeout))
|
||||
.build();
|
||||
|
||||
this.timeout = this.timeout.toBuilder().connectTimeout(Property.ofValue(connectTimeout)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder readTimeout(Duration readTimeout) {
|
||||
if (this.timeout == null) {
|
||||
this.timeout = TimeoutConfiguration.builder()
|
||||
.build();
|
||||
this.timeout = TimeoutConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.timeout = this.timeout.toBuilder()
|
||||
.readIdleTimeout(Property.ofValue(readTimeout))
|
||||
.build();
|
||||
|
||||
this.timeout = this.timeout.toBuilder().readIdleTimeout(Property.ofValue(readTimeout)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyType(Proxy.Type proxyType) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
this.proxy = ProxyConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.type(Property.ofValue(proxyType))
|
||||
.build();
|
||||
|
||||
this.proxy = this.proxy.toBuilder().type(Property.ofValue(proxyType)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyAddress(String proxyAddress) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
this.proxy = ProxyConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.address(Property.ofValue(proxyAddress))
|
||||
.build();
|
||||
|
||||
this.proxy = this.proxy.toBuilder().address(Property.ofValue(proxyAddress)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyPort(Integer proxyPort) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
this.proxy = ProxyConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.port(Property.ofValue(proxyPort))
|
||||
.build();
|
||||
|
||||
this.proxy = this.proxy.toBuilder().port(Property.ofValue(proxyPort)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyUsername(String proxyUsername) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
this.proxy = ProxyConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.username(Property.ofValue(proxyUsername))
|
||||
.build();
|
||||
|
||||
this.proxy = this.proxy.toBuilder().username(Property.ofValue(proxyUsername)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder proxyPassword(String proxyPassword) {
|
||||
if (this.proxy == null) {
|
||||
this.proxy = ProxyConfiguration.builder()
|
||||
.build();
|
||||
this.proxy = ProxyConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.proxy = this.proxy.toBuilder()
|
||||
.password(Property.ofValue(proxyPassword))
|
||||
.build();
|
||||
|
||||
this.proxy = this.proxy.toBuilder().password(Property.ofValue(proxyPassword)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder basicAuthUser(String basicAuthUser) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
this.auth = BasicAuthConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.username(Property.ofValue(basicAuthUser))
|
||||
.build();
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder().username(Property.ofValue(basicAuthUser)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -237,37 +208,21 @@ public class HttpConfiguration {
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder basicAuthPassword(String basicAuthPassword) {
|
||||
if (this.auth == null || !(this.auth instanceof BasicAuthConfiguration)) {
|
||||
this.auth = BasicAuthConfiguration.builder()
|
||||
.build();
|
||||
this.auth = BasicAuthConfiguration.builder().build();
|
||||
}
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder()
|
||||
.password(Property.ofValue(basicAuthPassword))
|
||||
.build();
|
||||
|
||||
this.auth = ((BasicAuthConfiguration) this.auth).toBuilder().password(Property.ofValue(basicAuthPassword)).build();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public HttpConfigurationBuilder logLevel(LogLevel logLevel) {
|
||||
if (logLevel == LogLevel.TRACE) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.REQUEST_BODY,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
LoggingType.RESPONSE_BODY
|
||||
};
|
||||
this.logs = new LoggingType[]{LoggingType.REQUEST_HEADERS, LoggingType.REQUEST_BODY, LoggingType.RESPONSE_HEADERS, LoggingType.RESPONSE_BODY};
|
||||
} else if (logLevel == LogLevel.DEBUG) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.REQUEST_HEADERS,
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
this.logs = new LoggingType[]{LoggingType.REQUEST_HEADERS, LoggingType.RESPONSE_HEADERS};
|
||||
} else if (logLevel == LogLevel.INFO) {
|
||||
this.logs = new LoggingType[]{
|
||||
LoggingType.RESPONSE_HEADERS,
|
||||
};
|
||||
this.logs = new LoggingType[]{LoggingType.RESPONSE_HEADERS};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@@ -14,5 +20,59 @@ import java.net.URL;
|
||||
public class ExternalPlugin {
|
||||
private final URL location;
|
||||
private final URL[] resources;
|
||||
private final long crc32;
|
||||
private volatile Long crc32; // lazy-val
|
||||
|
||||
public ExternalPlugin(URL location, URL[] resources) {
|
||||
this.location = location;
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
public Long getCrc32() {
|
||||
if (this.crc32 == null) {
|
||||
synchronized (this) {
|
||||
if (this.crc32 == null) {
|
||||
this.crc32 = computeJarCrc32(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a CRC32 of the JAR File without reading the whole file
|
||||
*
|
||||
* @param location of the JAR File.
|
||||
* @return the CRC32 of {@code -1} if the checksum can't be computed.
|
||||
*/
|
||||
private static long computeJarCrc32(final URL location) {
|
||||
CRC32 crc = new CRC32();
|
||||
try (JarFile jar = new JarFile(location.toURI().getPath(), false)) {
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
byte[] buffer = new byte[Long.BYTES]; // reusable buffer to avoid re-allocation
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
crc.update(entry.getName().getBytes(StandardCharsets.UTF_8));
|
||||
updateCrc32WithLong(crc, buffer, entry.getSize());
|
||||
updateCrc32WithLong(crc, buffer, entry.getCrc());
|
||||
}
|
||||
|
||||
return crc.getValue();
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCrc32WithLong(CRC32 crc32, byte[] reusable, long val) {
|
||||
// fast long -> byte conversion
|
||||
reusable[0] = (byte) (val >>> 56);
|
||||
reusable[1] = (byte) (val >>> 48);
|
||||
reusable[2] = (byte) (val >>> 40);
|
||||
reusable[3] = (byte) (val >>> 32);
|
||||
reusable[4] = (byte) (val >>> 24);
|
||||
reusable[5] = (byte) (val >>> 16);
|
||||
reusable[6] = (byte) (val >>> 8);
|
||||
reusable[7] = (byte) val;
|
||||
crc32.update(reusable);;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class PluginClassLoader extends URLClassLoader {
|
||||
+ "|dev.failsafe"
|
||||
+ "|reactor"
|
||||
+ "|io.opentelemetry"
|
||||
+ "|io.netty"
|
||||
+ ")\\..*$");
|
||||
|
||||
private final ClassLoader parent;
|
||||
|
||||
@@ -51,8 +51,7 @@ public class PluginResolver {
|
||||
final List<URL> resources = resolveUrlsForPluginPath(path);
|
||||
plugins.add(new ExternalPlugin(
|
||||
path.toUri().toURL(),
|
||||
resources.toArray(new URL[0]),
|
||||
computeJarCrc32(path)
|
||||
resources.toArray(new URL[0])
|
||||
));
|
||||
}
|
||||
} catch (final InvalidPathException | MalformedURLException e) {
|
||||
@@ -124,33 +123,5 @@ public class PluginResolver {
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute a CRC32 of the JAR File without reading the whole file
|
||||
*
|
||||
* @param location of the JAR File.
|
||||
* @return the CRC32 of {@code -1} if the checksum can't be computed.
|
||||
*/
|
||||
private static long computeJarCrc32(final Path location) {
|
||||
CRC32 crc = new CRC32();
|
||||
try (JarFile jar = new JarFile(location.toFile(), false)) {
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
crc.update(entry.getName().getBytes());
|
||||
crc.update(longToBytes(entry.getSize()));
|
||||
crc.update(longToBytes(entry.getCrc()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
return crc.getValue();
|
||||
}
|
||||
|
||||
private static byte[] longToBytes(long x) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
|
||||
buffer.putLong(x);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.Input;
|
||||
import io.kestra.core.models.flows.State;
|
||||
import io.kestra.core.models.flows.input.SecretInput;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.property.PropertyContext;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
@@ -282,15 +283,15 @@ public final class RunVariables {
|
||||
|
||||
if (flow != null && flow.getInputs() != null) {
|
||||
// we add default inputs value from the flow if not already set, this will be useful for triggers
|
||||
flow.getInputs().stream()
|
||||
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
|
||||
.forEach(input -> {
|
||||
try {
|
||||
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
|
||||
} catch (IllegalVariableEvaluationException e) {
|
||||
throw new RuntimeException("Unable to inject default value for input '" + input.getId() + "'", e);
|
||||
}
|
||||
});
|
||||
flow.getInputs().stream()
|
||||
.filter(input -> input.getDefaults() != null && !inputs.containsKey(input.getId()))
|
||||
.forEach(input -> {
|
||||
try {
|
||||
inputs.put(input.getId(), FlowInputOutput.resolveDefaultValue(input, propertyContext));
|
||||
} catch (IllegalVariableEvaluationException e) {
|
||||
// Silent catch, if an input depends on another input, or a variable that is populated at runtime / input filling time, we can't resolve it here.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!inputs.isEmpty()) {
|
||||
|
||||
@@ -180,23 +180,13 @@ public final class FileSerde {
|
||||
}
|
||||
|
||||
private static <T> MappingIterator<T> createMappingIterator(ObjectMapper objectMapper, Reader reader, TypeReference<T> type) throws IOException {
|
||||
// See https://github.com/FasterXML/jackson-dataformats-binary/issues/493
|
||||
// There is a limitation with the MappingIterator that cannot differentiate between an array of things (of whatever shape)
|
||||
// and a sequence/stream of things (of Array shape).
|
||||
// To work around that, we need to create a JsonParser and advance to the first token.
|
||||
try (var parser = objectMapper.createParser(reader)) {
|
||||
parser.nextToken();
|
||||
return objectMapper.readerFor(type).readValues(parser);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> MappingIterator<T> createMappingIterator(ObjectMapper objectMapper, Reader reader, Class<T> type) throws IOException {
|
||||
// See https://github.com/FasterXML/jackson-dataformats-binary/issues/493
|
||||
// There is a limitation with the MappingIterator that cannot differentiate between an array of things (of whatever shape)
|
||||
// and a sequence/stream of things (of Array shape).
|
||||
// To work around that, we need to create a JsonParser and advance to the first token.
|
||||
try (var parser = objectMapper.createParser(reader)) {
|
||||
parser.nextToken();
|
||||
return objectMapper.readerFor(type).readValues(parser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,7 @@ package io.kestra.core.services;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.kestra.core.exceptions.FlowProcessingException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.FlowId;
|
||||
import io.kestra.core.models.flows.FlowInterface;
|
||||
import io.kestra.core.models.flows.FlowWithException;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.flows.GenericFlow;
|
||||
import io.kestra.core.models.flows.*;
|
||||
import io.kestra.core.models.tasks.RunnableTask;
|
||||
import io.kestra.core.models.topologies.FlowTopology;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
@@ -30,16 +25,7 @@ import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -551,23 +537,24 @@ public class FlowService {
|
||||
return expandAll ? recursiveFlowTopology(new ArrayList<>(), tenant, namespace, id, destinationOnly) : flowTopologyRepository.get().findByFlow(tenant, namespace, id, destinationOnly).stream();
|
||||
}
|
||||
|
||||
private Stream<FlowTopology> recursiveFlowTopology(List<FlowId> flowIds, String tenantId, String namespace, String id, boolean destinationOnly) {
|
||||
private Stream<FlowTopology> recursiveFlowTopology(List<String> visitedTopologies, String tenantId, String namespace, String id, boolean destinationOnly) {
|
||||
if (flowTopologyRepository.isEmpty()) {
|
||||
throw noRepositoryException();
|
||||
}
|
||||
|
||||
List<FlowTopology> flowTopologies = flowTopologyRepository.get().findByFlow(tenantId, namespace, id, destinationOnly);
|
||||
|
||||
FlowId flowId = FlowId.of(tenantId, namespace, id, null);
|
||||
if (flowIds.contains(flowId)) {
|
||||
return flowTopologies.stream();
|
||||
}
|
||||
flowIds.add(flowId);
|
||||
var flowTopologies = flowTopologyRepository.get().findByFlow(tenantId, namespace, id, destinationOnly);
|
||||
|
||||
return flowTopologies.stream()
|
||||
.flatMap(topology -> Stream.of(topology.getDestination(), topology.getSource()))
|
||||
// recursively fetch child nodes
|
||||
.flatMap(node -> recursiveFlowTopology(flowIds, node.getTenantId(), node.getNamespace(), node.getId(), destinationOnly));
|
||||
// ignore already visited topologies
|
||||
.filter(x -> !visitedTopologies.contains(x.uid()))
|
||||
.flatMap(topology -> {
|
||||
visitedTopologies.add(topology.uid());
|
||||
Stream<FlowTopology> subTopologies = Stream
|
||||
.of(topology.getDestination(), topology.getSource())
|
||||
// recursively visit children and parents nodes
|
||||
.flatMap(relationNode -> recursiveFlowTopology(visitedTopologies, relationNode.getTenantId(), relationNode.getNamespace(), relationNode.getId(), destinationOnly));
|
||||
return Stream.concat(Stream.of(topology), subTopologies);
|
||||
});
|
||||
}
|
||||
|
||||
private IllegalStateException noRepositoryException() {
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.kestra.core.queues.QueueInterface;
|
||||
import io.kestra.core.runners.RunContext;
|
||||
import io.kestra.core.serializers.JacksonMapper;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.RetryUtils;
|
||||
import io.kestra.core.utils.TestsUtils;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import io.micronaut.http.HttpStatus;
|
||||
@@ -109,7 +110,8 @@ class HttpClientTest {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@RetryingTest(5) // Flaky on CI but never locally even with 100 repetitions
|
||||
@RetryingTest(5)
|
||||
// Flaky on CI but never locally even with 100 repetitions
|
||||
void getText() throws IllegalVariableEvaluationException, HttpClientException, IOException {
|
||||
Flow flow = TestsUtils.mockFlow();
|
||||
Execution execution = TestsUtils.mockExecution(flow, Map.of());
|
||||
@@ -292,7 +294,7 @@ class HttpClientTest {
|
||||
Map<String, Object> multipart = Map.of(
|
||||
"ping", "pong",
|
||||
"int", 1,
|
||||
"file", new File(Objects.requireNonNull(this.getClass().getClassLoader().getResource("logback.xml")).toURI()),
|
||||
"file", new File(Objects.requireNonNull(this.getClass().getClassLoader().getResource("logback.xml")).toURI()),
|
||||
"inputStream", new ByteArrayInputStream(IOUtils.toString(
|
||||
Objects.requireNonNull(this.getClass().getClassLoader().getResourceAsStream("logback.xml")),
|
||||
StandardCharsets.UTF_8
|
||||
@@ -310,8 +312,7 @@ class HttpClientTest {
|
||||
assertThat(response.getBody().get("ping")).isEqualTo("pong");
|
||||
assertThat(response.getBody().get("int")).isEqualTo("1");
|
||||
assertThat((String) response.getBody().get("file")).contains("logback");
|
||||
// @FIXME: Request seems to be correct, but not returned by micronaut
|
||||
// assertThat((String) response.getBody().get("inputStream"), containsString("logback"));
|
||||
assertThat((String) response.getBody().get("inputStream")).contains("logback");
|
||||
assertThat(response.getHeaders().firstValue(HttpHeaders.CONTENT_TYPE).orElseThrow()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
}
|
||||
@@ -321,12 +322,14 @@ class HttpClientTest {
|
||||
try (HttpClient client = client()) {
|
||||
URI uri = URI.create("http://localhost:1234");
|
||||
|
||||
HttpClientRequestException e = assertThrows(HttpClientRequestException.class, () -> {
|
||||
RetryUtils.RetryFailed retryFailed = assertThrows(RetryUtils.RetryFailed.class, () -> {
|
||||
client.request(HttpRequest.of(uri));
|
||||
});
|
||||
|
||||
assertThat(e.getRequest().getUri()).isEqualTo(uri);
|
||||
assertThat(e.getMessage()).contains("Connection refused");
|
||||
Throwable cause = retryFailed.getCause();
|
||||
assertThat(cause).isInstanceOf(org.apache.hc.client5.http.HttpHostConnectException.class);
|
||||
var e = (org.apache.hc.client5.http.HttpHostConnectException) cause;
|
||||
assertThat(e.getMessage()).contains("Connect to http://localhost:1234 failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
package io.kestra.core.runners;
|
||||
|
||||
import io.kestra.core.exceptions.IllegalVariableEvaluationException;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import io.kestra.core.models.flows.DependsOn;
|
||||
import io.kestra.core.models.flows.Flow;
|
||||
import io.kestra.core.models.flows.Type;
|
||||
import io.kestra.core.models.flows.input.BoolInput;
|
||||
import io.kestra.core.models.property.Property;
|
||||
import io.kestra.core.models.property.PropertyContext;
|
||||
import io.kestra.core.models.tasks.Task;
|
||||
import io.kestra.core.models.triggers.AbstractTrigger;
|
||||
import io.kestra.core.runners.pebble.functions.SecretFunction;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.micronaut.context.ApplicationContext;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -112,4 +123,25 @@ class RunVariablesTest {
|
||||
assertThat(kestra.get("environment")).isEqualTo("test");
|
||||
assertThat(kestra.get("url")).isEqualTo("http://localhost:8080");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonResolvableDynamicInputsShouldBeSkipped() throws IllegalVariableEvaluationException {
|
||||
Map<String, Object> variables = new RunVariables.DefaultBuilder()
|
||||
.withFlow(Flow
|
||||
.builder()
|
||||
.namespace("a.b")
|
||||
.id("c")
|
||||
.inputs(List.of(
|
||||
BoolInput.builder().id("a").type(Type.BOOL).defaults(Property.ofValue(true)).build(),
|
||||
BoolInput.builder().id("b").type(Type.BOOL).dependsOn(new DependsOn(List.of("a"), null)).defaults(Property.ofExpression("{{inputs.a == true}}")).build()
|
||||
))
|
||||
.build()
|
||||
)
|
||||
.withExecution(Execution.builder().id(IdUtils.create()).build())
|
||||
.build(new RunContextLogger(), PropertyContext.create(new VariableRenderer(Mockito.mock(ApplicationContext.class), Mockito.mock(VariableRenderer.VariableConfiguration.class), Collections.emptyList())));
|
||||
|
||||
Assertions.assertEquals(Map.of(
|
||||
"a", true
|
||||
), variables.get("inputs"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.kestra.core.server;
|
||||
|
||||
import io.kestra.core.contexts.KestraContext;
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import io.kestra.core.utils.Network;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -25,6 +26,7 @@ import java.util.Set;
|
||||
import static io.kestra.core.server.ServiceStateTransition.Result.ABORTED;
|
||||
import static io.kestra.core.server.ServiceStateTransition.Result.FAILED;
|
||||
import static io.kestra.core.server.ServiceStateTransition.Result.SUCCEEDED;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith({MockitoExtension.class})
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -59,6 +61,8 @@ public class ServiceLivenessManagerTest {
|
||||
);
|
||||
|
||||
KestraContext context = Mockito.mock(KestraContext.class);
|
||||
KestraContext.setContext(context);
|
||||
when(context.getServerType()).thenReturn(ServerType.INDEXER);
|
||||
this.serviceLivenessManager = new ServiceLivenessManager(
|
||||
config,
|
||||
new ServiceRegistry(),
|
||||
@@ -100,8 +104,7 @@ public class ServiceLivenessManagerTest {
|
||||
);
|
||||
|
||||
// mock the state transition result
|
||||
Mockito
|
||||
.when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
// When
|
||||
@@ -127,8 +130,7 @@ public class ServiceLivenessManagerTest {
|
||||
);
|
||||
|
||||
// mock the state transition result
|
||||
Mockito
|
||||
.when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
// When
|
||||
@@ -147,8 +149,7 @@ public class ServiceLivenessManagerTest {
|
||||
serviceLivenessManager.updateServiceInstance(running, serviceInstanceFor(running));
|
||||
|
||||
// mock the state transition result
|
||||
Mockito
|
||||
.when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
when(serviceLivenessUpdater.update(Mockito.any(ServiceInstance.class), Mockito.any(Service.ServiceState.class)))
|
||||
.thenReturn(new ServiceStateTransition.Response(ABORTED));
|
||||
|
||||
// When
|
||||
|
||||
@@ -111,4 +111,11 @@ class SanityCheckTest {
|
||||
assertThat(execution.getTaskRunList()).hasSize(6);
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExecuteFlow("sanity-checks/output_values.yaml")
|
||||
void qaOutputValues(Execution execution) {
|
||||
assertThat(execution.getTaskRunList()).hasSize(2);
|
||||
assertThat(execution.getState().getCurrent()).isEqualTo(State.Type.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
package io.kestra.core.topologies;
|
||||
|
||||
import io.kestra.core.exceptions.FlowProcessingException;
|
||||
import io.kestra.core.junit.annotations.KestraTest;
|
||||
import io.kestra.core.models.flows.FlowWithSource;
|
||||
import io.kestra.core.models.topologies.FlowTopology;
|
||||
import io.kestra.core.repositories.FlowRepositoryInterface;
|
||||
import io.kestra.core.repositories.FlowTopologyRepositoryInterface;
|
||||
import io.kestra.core.services.FlowService;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import jakarta.inject.Inject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@KestraTest
|
||||
public class FlowTopologyTest {
|
||||
@Inject
|
||||
private FlowService flowService;
|
||||
@Inject
|
||||
private FlowTopologyService flowTopologyService;
|
||||
@Inject
|
||||
private FlowTopologyRepositoryInterface flowTopologyRepository;
|
||||
|
||||
@Test
|
||||
void should_findDependencies_simpleCase() throws FlowProcessingException {
|
||||
// Given
|
||||
var tenantId = randomTenantId();
|
||||
var child = flowService.importFlow(tenantId,
|
||||
"""
|
||||
id: child
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
""");
|
||||
var parent = flowService.importFlow(tenantId, """
|
||||
id: parent
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: subflow
|
||||
type: io.kestra.core.tasks.flows.Flow
|
||||
flowId: child
|
||||
namespace: io.kestra.unittest
|
||||
""");
|
||||
var unrelatedFlow = flowService.importFlow(tenantId, """
|
||||
id: unrelated_flow
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
""");
|
||||
|
||||
// When
|
||||
computeAndSaveTopologies(List.of(child, parent, unrelatedFlow));
|
||||
System.out.println();
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
// Then
|
||||
assertThat(dependencies.map(FlowTopologyTestData::of))
|
||||
.containsExactlyInAnyOrder(
|
||||
new FlowTopologyTestData(parent, child)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_findDependencies_subchildAndSuperParent() throws FlowProcessingException {
|
||||
// Given
|
||||
var tenantId = randomTenantId();
|
||||
var subChild = flowService.importFlow(tenantId,
|
||||
"""
|
||||
id: sub_child
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
""");
|
||||
var child = flowService.importFlow(tenantId,
|
||||
"""
|
||||
id: child
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: subflow
|
||||
type: io.kestra.core.tasks.flows.Flow
|
||||
flowId: sub_child
|
||||
namespace: io.kestra.unittest
|
||||
""");
|
||||
var superParent = flowService.importFlow(tenantId, """
|
||||
id: super_parent
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: subflow
|
||||
type: io.kestra.core.tasks.flows.Flow
|
||||
flowId: parent
|
||||
namespace: io.kestra.unittest
|
||||
""");
|
||||
var parent = flowService.importFlow(tenantId, """
|
||||
id: parent
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: subflow
|
||||
type: io.kestra.core.tasks.flows.Flow
|
||||
flowId: child
|
||||
namespace: io.kestra.unittest
|
||||
""");
|
||||
var unrelatedFlow = flowService.importFlow(tenantId, """
|
||||
id: unrelated_flow
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
""");
|
||||
|
||||
// When
|
||||
computeAndSaveTopologies(List.of(subChild, child, superParent, parent, unrelatedFlow));
|
||||
System.out.println();
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
System.out.println();
|
||||
|
||||
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
// Then
|
||||
assertThat(dependencies.map(FlowTopologyTestData::of))
|
||||
.containsExactlyInAnyOrder(
|
||||
new FlowTopologyTestData(superParent, parent),
|
||||
new FlowTopologyTestData(parent, child),
|
||||
new FlowTopologyTestData(child, subChild)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_findDependencies_cyclicTriggers() throws FlowProcessingException {
|
||||
// Given
|
||||
var tenantId = randomTenantId();
|
||||
var triggeredFlowOne = flowService.importFlow(tenantId,
|
||||
"""
|
||||
id: triggered_flow_one
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
triggers:
|
||||
- id: listen
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
conditions:
|
||||
- type: io.kestra.plugin.core.condition.ExecutionStatus
|
||||
in:
|
||||
- FAILED
|
||||
""");
|
||||
var triggeredFlowTwo = flowService.importFlow(tenantId, """
|
||||
id: triggered_flow_two
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
triggers:
|
||||
- id: listen
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
conditions:
|
||||
- type: io.kestra.plugin.core.condition.ExecutionStatus
|
||||
in:
|
||||
- FAILED
|
||||
""");
|
||||
|
||||
// When
|
||||
computeAndSaveTopologies(List.of(triggeredFlowOne, triggeredFlowTwo));
|
||||
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", triggeredFlowTwo.getId(), false, true).toList();
|
||||
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
// Then
|
||||
assertThat(dependencies.stream().map(FlowTopologyTestData::of))
|
||||
.containsExactlyInAnyOrder(
|
||||
new FlowTopologyTestData(triggeredFlowTwo, triggeredFlowOne),
|
||||
new FlowTopologyTestData(triggeredFlowOne, triggeredFlowTwo)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void flowTriggerWithTargetFlow() throws FlowProcessingException {
|
||||
// Given
|
||||
var tenantId = randomTenantId();
|
||||
var parent = flowService.importFlow(tenantId,
|
||||
"""
|
||||
id: parent
|
||||
namespace: io.kestra.unittest
|
||||
inputs:
|
||||
- id: a
|
||||
type: BOOL
|
||||
defaults: true
|
||||
|
||||
- id: b
|
||||
type: BOOL
|
||||
defaults: "{{ inputs.a == true }}"
|
||||
dependsOn:
|
||||
inputs:
|
||||
- a
|
||||
tasks:
|
||||
- id: helloA
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello A
|
||||
""");
|
||||
var child = flowService.importFlow(tenantId, """
|
||||
id: child
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: helloB
|
||||
type: io.kestra.plugin.core.log.Log
|
||||
message: Hello B
|
||||
triggers:
|
||||
- id: release
|
||||
type: io.kestra.plugin.core.trigger.Flow
|
||||
states:
|
||||
- SUCCESS
|
||||
preconditions:
|
||||
id: flows
|
||||
flows:
|
||||
- namespace: io.kestra.unittest
|
||||
flowId: parent
|
||||
""");
|
||||
var unrelatedFlow = flowService.importFlow(tenantId, """
|
||||
id: unrelated_flow
|
||||
namespace: io.kestra.unittest
|
||||
tasks:
|
||||
- id: download
|
||||
type: io.kestra.plugin.core.http.Download
|
||||
""");
|
||||
|
||||
// When
|
||||
computeAndSaveTopologies(List.of(child, parent, unrelatedFlow));
|
||||
System.out.println();
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
var dependencies = flowService.findDependencies(tenantId, "io.kestra.unittest", parent.getId(), false, true);
|
||||
flowTopologyRepository.findAll(tenantId).forEach(topology -> {
|
||||
System.out.println(FlowTopologyTestData.of(topology));
|
||||
});
|
||||
|
||||
// Then
|
||||
assertThat(dependencies.map(FlowTopologyTestData::of))
|
||||
.containsExactlyInAnyOrder(
|
||||
new FlowTopologyTestData(parent, child)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* this function mimics the production behaviour
|
||||
*/
|
||||
private void computeAndSaveTopologies(List<@NotNull FlowWithSource> flows) {
|
||||
flows.forEach(flow ->
|
||||
flowTopologyService
|
||||
.topology(
|
||||
flow,
|
||||
flows
|
||||
).distinct()
|
||||
.forEach(topology -> flowTopologyRepository.save(topology))
|
||||
);
|
||||
}
|
||||
|
||||
private static String randomTenantId() {
|
||||
return FlowTopologyTest.class + IdUtils.create();
|
||||
}
|
||||
|
||||
|
||||
record FlowTopologyTestData(String sourceUid, String destinationUid) {
|
||||
public FlowTopologyTestData(FlowWithSource parent, FlowWithSource child) {
|
||||
this(parent.uidWithoutRevision(), child.uidWithoutRevision());
|
||||
}
|
||||
|
||||
public static FlowTopologyTestData of(FlowTopology flowTopology) {
|
||||
return new FlowTopologyTestData(flowTopology.getSource().getUid(), flowTopology.getDestination().getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return sourceUid + " -> " + destinationUid;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
core/src/test/resources/sanity-checks/output_values.yaml
Normal file
27
core/src/test/resources/sanity-checks/output_values.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
id: output_values
|
||||
namespace: sanitychecks.core
|
||||
|
||||
variables:
|
||||
var1: "myvaribale"
|
||||
var2: 25
|
||||
|
||||
tasks:
|
||||
- id: output_values
|
||||
type: io.kestra.plugin.core.output.OutputValues
|
||||
values:
|
||||
string_value: "hello"
|
||||
number_value: 42
|
||||
nested_object:
|
||||
key1: "value1"
|
||||
key2: "value2"
|
||||
text_var: "{{ vars.var1}}"
|
||||
number_var: "the number value is: {{vars.var2}}"
|
||||
|
||||
- id: assert
|
||||
type: io.kestra.plugin.core.execution.Assert
|
||||
conditions:
|
||||
- "{{ outputs.output_values.values.string_value == 'hello'}}"
|
||||
- "{{ outputs.output_values.values.number_value == 42 }}"
|
||||
- "{{ outputs.output_values.values.nested_object['key1'] == 'value1' }}"
|
||||
- "{{ outputs.output_values.values.text_var == 'myvaribale' }}"
|
||||
- "{{ outputs.output_values.values.number_var == 'the number value is: 25' }}"
|
||||
4
dev-tools/kestra-devtools/.gitignore
vendored
Normal file
4
dev-tools/kestra-devtools/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.DS_Store
|
||||
6
dev-tools/kestra-devtools/.prettierrc.json
Normal file
6
dev-tools/kestra-devtools/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
12
dev-tools/kestra-devtools/eslint.config.mjs
Normal file
12
dev-tools/kestra-devtools/eslint.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-check
|
||||
import eslint from "@eslint/js";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: ["dist/**", "coverage/**", "node_modules/**"],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
);
|
||||
7175
dev-tools/kestra-devtools/package-lock.json
generated
Normal file
7175
dev-tools/kestra-devtools/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
dev-tools/kestra-devtools/package.json
Normal file
51
dev-tools/kestra-devtools/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "kestra-devtools-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "a CLI tool to run various dev tasks to build, test, release Kestra",
|
||||
"bin": {
|
||||
"my-cli": "dist/kestra-devtools-cli.cjs"
|
||||
},
|
||||
"main": "dist/kestra-devtools-cli.cjs",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vitest --watch",
|
||||
"build": "vite build && tsc -p tsconfig.types.json",
|
||||
"test": "npm run lint && vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/kestra-devtools-cli.cjs",
|
||||
"link": "npm link",
|
||||
"unlink": "npm unlink -g my-cli || true"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.43.0",
|
||||
"@typescript-eslint/parser": "^8.43.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vite": "^7.1.5",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"octokit": "^5.0.3"
|
||||
}
|
||||
}
|
||||
13
dev-tools/kestra-devtools/src/github-api.ts
Normal file
13
dev-tools/kestra-devtools/src/github-api.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Octokit} from "octokit";
|
||||
|
||||
export async function commentPR(githubToken: string, owner: string, repo: string, prNumber: number, content: string){
|
||||
const octokit = new Octokit({ auth: githubToken });
|
||||
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number:prNumber,
|
||||
body: content,
|
||||
});
|
||||
}
|
||||
|
||||
15
dev-tools/kestra-devtools/src/github-context.ts
Normal file
15
dev-tools/kestra-devtools/src/github-context.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import core from '@actions/core';
|
||||
import {context} from '@actions/github';
|
||||
import {strict as assert} from 'assert';
|
||||
|
||||
export function getPRContext():{token: string, owner: string, repo: string, prNumber: number}{
|
||||
const GITHUB_TOKEN = core.getInput('GITHUB_TOKEN') || process.env.GITHUB_TOKEN;
|
||||
|
||||
assert.ok(GITHUB_TOKEN, "GITHUB_TOKEN is mandatory");
|
||||
assert.ok(context.issue);
|
||||
assert.ok(context.issue.owner);
|
||||
assert.ok(context.issue.repo);
|
||||
assert.ok(context.issue.number);
|
||||
|
||||
return {token: GITHUB_TOKEN, owner: context.repo.owner, repo: context.repo.repo, prNumber: context.issue.number }
|
||||
}
|
||||
18
dev-tools/kestra-devtools/src/kestra-devtools-cli.test.ts
Normal file
18
dev-tools/kestra-devtools/src/kestra-devtools-cli.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { main } from "./kestra-devtools-cli";
|
||||
|
||||
describe("cli tests", () => {
|
||||
it("prints hello with default", async () => {
|
||||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await main(["node", "cli"]);
|
||||
expect(spy).toHaveBeenCalledWith("Hello, world!");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("prints hello with name", async () => {
|
||||
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await main(["node", "cli", "Roman"]);
|
||||
expect(spy).toHaveBeenCalledWith("Hello, Roman!");
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
88
dev-tools/kestra-devtools/src/kestra-devtools-cli.ts
Normal file
88
dev-tools/kestra-devtools/src/kestra-devtools-cli.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// Simple CLI entry point.
|
||||
// Built to dist/kestra-devtools-cli.cjs with a shebang so it can be executed directly.
|
||||
|
||||
import { getWorkingDir } from "./utilities/working-dir";
|
||||
import {exportTestReportSummary} from "./tests-reporting/export-test-report-summary";
|
||||
import {getPRContext} from "./github-context";
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
// argv[0] = node, argv[1] = script, rest are args
|
||||
const args = argv.slice(2);
|
||||
const flags: Record<string, string | boolean> = {};
|
||||
const positionals: string[] = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a.startsWith("--")) {
|
||||
const [k, v] = a.slice(2).split("=");
|
||||
flags[k] = v ?? true;
|
||||
} else if (a.startsWith("-") && a.length > 1) {
|
||||
const letters = a.slice(1).split("");
|
||||
letters.forEach((l) => (flags[l] = true));
|
||||
} else {
|
||||
positionals.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
return { flags, positionals };
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv) {
|
||||
const { flags, positionals } = parseArgs(argv);
|
||||
|
||||
if (flags.h || flags.help) {
|
||||
console.log(`kestra-devtools-cli
|
||||
|
||||
Usage:
|
||||
kestra-devtools-cli [options] [name]
|
||||
|
||||
Options:
|
||||
-h, --help Show help
|
||||
-v, --version Show version
|
||||
|
||||
Examples:
|
||||
kestra-devtools-cli generateTestReportSummary /Users/roman/Documents/git-repos/kestra --only-errors
|
||||
|
||||
`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (positionals[0] === "generateTestReportSummary") {
|
||||
const dirArg = positionals[1];
|
||||
if (!dirArg) {
|
||||
console.error(
|
||||
"Error: missing working directory argument.\nUsage: kestra-devtools-cli generateTestReportSummary <absolute-path>",
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
const ci = Boolean(flags["ci"]);
|
||||
const workingDir = getWorkingDir(dirArg);
|
||||
const summary = await exportTestReportSummary(workingDir, {
|
||||
onlyErrors: Boolean(flags["only-errors"]),
|
||||
githubContext: ci ? getPRContext() : undefined
|
||||
});
|
||||
// Print to stdout so it can be piped in CI or viewed in terminal
|
||||
console.log(summary);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (flags.v || flags.version) {
|
||||
// package.json is not bundled by default; prefer env-injected version if needed.
|
||||
console.log("kestra-devtools-cli v0.1.0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const name = positionals[0] ?? "world";
|
||||
console.log(`Hello, ${name}!`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If executed directly, run main()
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main()
|
||||
.then((code) => process.exit(code))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {commentPR} from "../github-api";
|
||||
import {WorkingDir} from "../utilities/working-dir";
|
||||
import {generateTestReportSummary} from "./generate-test-report-summary";
|
||||
import {strict as assert} from 'assert';
|
||||
|
||||
export async function exportTestReportSummary(workingDir: WorkingDir, options?: {
|
||||
onlyErrors?: boolean,
|
||||
githubContext?: { token: string, owner: string, repo: string, prNumber: number }
|
||||
}) {
|
||||
const report = await generateTestReportSummary(workingDir, {onlyErrors: options?.onlyErrors})
|
||||
if (options?.githubContext) {
|
||||
assert.ok(options.githubContext.token, "github token is mandatory");
|
||||
assert.ok(options.githubContext.owner);
|
||||
assert.ok(options.githubContext.repo);
|
||||
assert.ok(options.githubContext.prNumber);
|
||||
|
||||
await commentPR(options.githubContext.token, options.githubContext.owner, options.githubContext.repo, options.githubContext.prNumber, report);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getJavaProjectNameFromBuildAbsolutePath } from "./file-path-utils";
|
||||
|
||||
describe("test getJavaProjectNameFromBuildAbsolutePath", () => {
|
||||
it("should work for Kestra modules paths", async () => {
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/Users/roman/Documents/git-repos/kestra/core/build/test-results/junit/TEST-io.kestra.core.validations.ScheduleValidationTest.xml",
|
||||
),
|
||||
).toEqual("core");
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/kestra/runner-memory/build/test-results/junit/open-test-report.xml",
|
||||
),
|
||||
).toEqual("runner-memory");
|
||||
expect(
|
||||
getJavaProjectNameFromBuildAbsolutePath(
|
||||
"/kestra-ee/executor/build/test-results/junit/open-test-report.xml",
|
||||
),
|
||||
).toEqual("executor");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export function getJavaProjectNameFromBuildAbsolutePath(absoluteFilePath: string): string {
|
||||
const parts = absoluteFilePath.split("/");
|
||||
const buildIndex = parts.lastIndexOf("build");
|
||||
if (buildIndex > 0) {
|
||||
return parts[buildIndex - 1];
|
||||
}
|
||||
|
||||
// return full path if not handled
|
||||
return absoluteFilePath;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseJunitModuleReport } from "./parse-junit-module-report";
|
||||
|
||||
describe("parse-junit-report test", () => {
|
||||
it("parse OK for all tests success", async () => {
|
||||
const junitReport = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="io.kestra.core.validations.ScheduleValidationTest" tests="6" skipped="0" failures="0" errors="0" timestamp="2025-09-11T17:32:18.116Z" hostname="Romans-MacBook-Pro.local" time="0.202">
|
||||
<properties/>
|
||||
<testcase name="sundayDayOfTheWeekAlias()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="withSecondsValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="lateMaximumDelayValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="intervalValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<testcase name="nicknameValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.203"/>
|
||||
<testcase name="cronValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.202"/>
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
</testsuite>
|
||||
`;
|
||||
|
||||
const res = parseJunitModuleReport(junitReport);
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(res.testsuites).toEqual([
|
||||
{
|
||||
name: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
errors: 0,
|
||||
failures: 0,
|
||||
skipped: 0,
|
||||
success: 6,
|
||||
tests: 6,
|
||||
status: "success",
|
||||
time: 0.202,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "withSecondsValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "lateMaximumDelayValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "intervalValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "nicknameValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.203,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "cronValidation()",
|
||||
classname: "io.kestra.core.validations.ScheduleValidationTest",
|
||||
time: 0.202,
|
||||
status: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("parse OK for test in error", async () => {
|
||||
const junitReport = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite name="io.kestra.core.validations.ScheduleValidationTest" tests="1" skipped="0" failures="1" errors="0" timestamp="2025-09-11T17:56:02.292Z" hostname="Romans-MacBook-Pro.local" time="0.265">
|
||||
<properties/>
|
||||
<testcase name="intervalValidation()" classname="io.kestra.core.validations.ScheduleValidationTest" time="0.043">
|
||||
<failure message="java.lang.RuntimeException: I failed and this is my log" type="java.lang.RuntimeException">java.lang.RuntimeException: I failed and this is my log
|
||||
\tat io.kestra.core.validations.ScheduleValidationTest.intervalValidation(ScheduleValidationTest.java:93)
|
||||
\tat java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
||||
\tat io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
|
||||
\tat io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:162)
|
||||
\tat io.micronaut.test.extensions.AbstractMicronautExtension.interceptTest(AbstractMicronautExtension.java:119)
|
||||
\tat io.micronaut.test.extensions.junit5.MicronautJunit5Extension.interceptTestMethod(MicronautJunit5Extension.java:129)
|
||||
\tat java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
|
||||
\tat java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
|
||||
\tat java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
|
||||
</failure>
|
||||
</testcase>
|
||||
<system-out><![CDATA[]]></system-out>
|
||||
<system-err><![CDATA[]]></system-err>
|
||||
</testsuite>
|
||||
`;
|
||||
|
||||
const res = parseJunitModuleReport(junitReport);
|
||||
|
||||
expect(res.testsuites).length(1);
|
||||
expect(res.testsuites[0].testcases).length(1);
|
||||
expect(res.testsuites[0].testcases[0].status).equal("failed");
|
||||
expect(res.testsuites[0].testcases[0].message).contain("I failed and this is my log");
|
||||
expect(res.testsuites[0].testcases[0].details).contain("I failed and this is my log");
|
||||
expect(res.testsuites[0].testcases[0].details).contain("ForkJoinWorkerThread");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
|
||||
export type JUnitModuleReport = {
|
||||
suites: number;
|
||||
tests: number;
|
||||
failures: number;
|
||||
errors: number;
|
||||
skipped: number;
|
||||
success: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
time: number; // total duration in seconds
|
||||
testsuites: Array<JunitTestSuite>;
|
||||
};
|
||||
|
||||
export interface JunitTestSuite {
|
||||
name?: string;
|
||||
tests: number;
|
||||
failures: number;
|
||||
errors: number;
|
||||
skipped: number;
|
||||
success: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
time: number;
|
||||
testcases: Array<JunitTestCase>;
|
||||
}
|
||||
|
||||
export interface JunitTestCase {
|
||||
classname?: string;
|
||||
name: string;
|
||||
time?: number;
|
||||
status: "success" | "failed" | "error" | "skipped";
|
||||
message?: string;
|
||||
type?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
// for more info on the Junit test report format = https://github.com/testmoapp/junitxml
|
||||
export function parseJunitModuleReport(xml: string): JUnitModuleReport {
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: "",
|
||||
allowBooleanAttributes: true,
|
||||
parseAttributeValue: true,
|
||||
trimValues: false,
|
||||
});
|
||||
|
||||
const obj = parser.parse(xml);
|
||||
|
||||
// JUnit can be either <testsuites> or a single <testsuite>
|
||||
const rawSuites = obj?.testsuites?.testsuite ?? obj?.testsuite ?? [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const suites = toArray<any>(rawSuites);
|
||||
|
||||
const report: JUnitModuleReport = {
|
||||
suites: suites.length,
|
||||
tests: 0,
|
||||
failures: 0,
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
success: 0,
|
||||
status: "success",
|
||||
time: 0,
|
||||
testsuites: [],
|
||||
};
|
||||
|
||||
for (const s of suites) {
|
||||
const name: string | undefined = s.name;
|
||||
|
||||
// Attributes may exist on the suite OR we may need to infer from testcases
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const testcases = toArray<any>(s.testcase ?? []);
|
||||
|
||||
const suiteCounts = {
|
||||
tests: numeric(s.tests, testcases.length),
|
||||
failures: numeric(s.failures, 0),
|
||||
errors: numeric(s.errors, 0),
|
||||
skipped: numeric(s.skipped, 0),
|
||||
time: numeric(s.time, sum(testcases.map((tc) => numeric(tc.time, 0)))),
|
||||
};
|
||||
|
||||
// If suite attributes missing, infer from testcases
|
||||
if (
|
||||
!isFiniteNumber(s.failures) ||
|
||||
!isFiniteNumber(s.errors) ||
|
||||
!isFiniteNumber(s.skipped)
|
||||
) {
|
||||
let f = 0,
|
||||
e = 0,
|
||||
sk = 0;
|
||||
for (const tc of testcases) {
|
||||
if (hasKey(tc, "failed")) f += toArray(tc.failed).length;
|
||||
if (hasKey(tc, "error")) e += toArray(tc.error).length;
|
||||
if (hasKey(tc, "skipped")) sk += toArray(tc.skipped).length || 1; // some producers put empty <skipped/>
|
||||
}
|
||||
if (!isFiniteNumber(suiteCounts.failures)) suiteCounts.failures = f;
|
||||
if (!isFiniteNumber(suiteCounts.errors)) suiteCounts.errors = e;
|
||||
if (!isFiniteNumber(suiteCounts.skipped)) suiteCounts.skipped = sk;
|
||||
}
|
||||
|
||||
const successCount =
|
||||
suiteCounts.tests - suiteCounts.errors - suiteCounts.failures - suiteCounts.skipped;
|
||||
|
||||
let suiteStatus: "success" | "failed" | "error" | "skipped" = "success";
|
||||
if (suiteCounts.skipped === suiteCounts.tests) {
|
||||
suiteStatus = "skipped";
|
||||
} else if (suiteCounts.errors > 0) {
|
||||
suiteStatus = "error";
|
||||
} else if (suiteCounts.failures > 0) {
|
||||
suiteStatus = "failed";
|
||||
}
|
||||
|
||||
const suiteDetail: JunitTestSuite = {
|
||||
name,
|
||||
tests: suiteCounts.tests,
|
||||
failures: suiteCounts.failures,
|
||||
errors: suiteCounts.errors,
|
||||
skipped: suiteCounts.skipped,
|
||||
success: successCount,
|
||||
status: suiteStatus,
|
||||
time: suiteCounts.time,
|
||||
testcases: [],
|
||||
};
|
||||
|
||||
// Collect failed tests and build suiteDetail.testcases
|
||||
for (const tc of testcases) {
|
||||
const classname: string | undefined = tc.classname;
|
||||
const nameTc: string = tc.name;
|
||||
const time: number | undefined = isFiniteNumber(tc.time) ? Number(tc.time) : undefined;
|
||||
|
||||
// Determine status
|
||||
if (tc.failure) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "failed",
|
||||
message: tc.failure.message,
|
||||
type: tc.failure.type,
|
||||
details: textContent(tc.failure),
|
||||
});
|
||||
} else if (tc.error) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "error",
|
||||
message: tc.error.message,
|
||||
type: tc.error.message.type,
|
||||
details: textContent(tc.error),
|
||||
});
|
||||
} else if (tc.skipped) {
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "skipped",
|
||||
message: tc.skipped.message,
|
||||
details: textContent(tc.skipped),
|
||||
});
|
||||
} else {
|
||||
// success test
|
||||
suiteDetail.testcases.push({
|
||||
classname,
|
||||
name: nameTc,
|
||||
time,
|
||||
status: "success",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
report.tests += suiteCounts.tests;
|
||||
report.failures += suiteCounts.failures;
|
||||
report.errors += suiteCounts.errors;
|
||||
report.skipped += suiteCounts.skipped;
|
||||
report.success += suiteDetail.success;
|
||||
report.time += suiteCounts.time;
|
||||
|
||||
report.testsuites.push(suiteDetail);
|
||||
}
|
||||
|
||||
if (report.skipped === report.tests) {
|
||||
report.status = "skipped";
|
||||
} else if (report.errors > 0) {
|
||||
report.status = "error";
|
||||
} else if (report.failures > 0) {
|
||||
report.status = "failed";
|
||||
} else {
|
||||
report.status = "success";
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: parse a file from disk.
|
||||
*/
|
||||
export async function summarizeJunitReportFromFile(filePath: string): Promise<JUnitModuleReport> {
|
||||
const xml = await fs.readFile(filePath, "utf8");
|
||||
return parseJunitModuleReport(xml);
|
||||
}
|
||||
|
||||
// -------------------- helpers --------------------
|
||||
|
||||
function toArray<T>(v: T | T[] | undefined | null): T[] {
|
||||
if (v == null) return [];
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
function numeric<T>(value: T, fallback = 0): number {
|
||||
const n = Number(value as unknown);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function sum(nums: number[]): number {
|
||||
return nums.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
function isFiniteNumber(v: unknown): v is number {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
// Some producers put the text of <failed> / <error> inside `#text` or as the value itself.
|
||||
function textContent(node: unknown): string | undefined {
|
||||
if (node == null) return undefined;
|
||||
if (typeof node === "string") return node;
|
||||
if (isRecord(node) && typeof node["#text"] === "string") {
|
||||
return node["#text"] as string;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasKey<O>(obj: O, key: PropertyKey): key is keyof O {
|
||||
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { summarizeJunitReport, TestReport } from "./summarize-junit-report";
|
||||
|
||||
describe("summarize-junit-report test", () => {
|
||||
const testReportsWithGreenTests: TestReport[] = [
|
||||
{
|
||||
projectName: "java-module-1",
|
||||
projectReport: {
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 0,
|
||||
success: 1,
|
||||
status: "success",
|
||||
tests: 1,
|
||||
time: 3,
|
||||
suites: 1,
|
||||
testsuites: [
|
||||
{
|
||||
name: "io.kestra.core.some.Test",
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 0,
|
||||
success: 1,
|
||||
status: "success",
|
||||
tests: 1,
|
||||
time: 3,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.some.Test",
|
||||
time: 3,
|
||||
status: "success",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
it("summarizeJunitReport for one green module", async () => {
|
||||
const res = summarizeJunitReport(testReportsWithGreenTests);
|
||||
|
||||
expect(res.hasErrors).equal(false);
|
||||
expect(res.markdownContent).contains("java-module-1");
|
||||
expect((res.markdownContent.match(/java-module-1/g) || []).length).toBe(2);// should appear twice
|
||||
expect(res.markdownContent).contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("io.kestra.core.some.Test");
|
||||
});
|
||||
|
||||
it("summarizeJunitReport for one green module should not print tests when onlyErrors:true", async () => {
|
||||
const res = summarizeJunitReport(testReportsWithGreenTests, { onlyErrors: true });
|
||||
|
||||
expect(res.hasErrors).equal(false);
|
||||
expect(res.markdownContent).contains("java-module-1");
|
||||
expect(res.markdownContent).not.contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).not.contains(
|
||||
"io.kestra.core.validations.ScheduleValidationTest",
|
||||
);
|
||||
});
|
||||
|
||||
const testReportWithFailedTests: TestReport[] = [
|
||||
{
|
||||
projectName: "java-module-1",
|
||||
projectReport: {
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 1,
|
||||
success: 1,
|
||||
status: "failed",
|
||||
tests: 2,
|
||||
time: 3,
|
||||
suites: 1,
|
||||
testsuites: [
|
||||
{
|
||||
name: "io.kestra.core.someother.Test2",
|
||||
errors: 0,
|
||||
skipped: 0,
|
||||
failures: 1,
|
||||
success: 1,
|
||||
status: "failed",
|
||||
tests: 2,
|
||||
time: 3,
|
||||
testcases: [
|
||||
{
|
||||
name: "sundayDayOfTheWeekAlias()",
|
||||
classname: "io.kestra.core.someother.Test2",
|
||||
time: 3,
|
||||
status: "success",
|
||||
},
|
||||
{
|
||||
name: "failingTest()",
|
||||
classname: "io.kestra.core.someother.Test2",
|
||||
time: 3,
|
||||
status: "failed",
|
||||
message: "java.lang.RuntimeException: I failed and this is my log",
|
||||
details: "this is the error logs details",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
it("summarizeJunitReport for failed tests should summarize all by default without details", async () => {
|
||||
const res = summarizeJunitReport(testReportWithFailedTests);
|
||||
|
||||
expect(res.hasErrors).equal(true);
|
||||
expect(res.markdownContent).contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("failingTest()");
|
||||
expect(res.markdownContent).contains(
|
||||
"java.lang.RuntimeException: I failed and this is my log",
|
||||
);
|
||||
expect(res.markdownContent).not.contains("this is the error logs details");
|
||||
});
|
||||
it("summarizeJunitReport for failed tests should summarize only errors with details when onlyErrors:true", async () => {
|
||||
const res = summarizeJunitReport(testReportWithFailedTests, { onlyErrors: true });
|
||||
|
||||
expect(res.hasErrors).equal(true);
|
||||
expect(res.markdownContent).not.contains("sundayDayOfTheWeekAlias()");
|
||||
expect(res.markdownContent).contains("failingTest()");
|
||||
expect(res.markdownContent).contains(
|
||||
"java.lang.RuntimeException: I failed and this is my log",
|
||||
);
|
||||
expect(res.markdownContent).contains("this is the error logs details");
|
||||
});
|
||||
|
||||
it("summarizeJunitReport should merge module reports", async () => {
|
||||
// given 2 reports for the same module, but for different tests
|
||||
const reports = [...testReportsWithGreenTests, ...testReportWithFailedTests]
|
||||
const res = summarizeJunitReport(reports, { onlyErrors: true });
|
||||
|
||||
expect(res.hasErrors).equal(true);
|
||||
expect(res.markdownContent).contain("java-module-1");
|
||||
|
||||
// it should not be duplicated
|
||||
expect((res.markdownContent.match(/java-module-1/g) || []).length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
import { JUnitModuleReport } from "./parse-junit-module-report";
|
||||
|
||||
export type MarkdownString = string;
|
||||
|
||||
export interface TestReport {
|
||||
projectName: string;
|
||||
projectReport: JUnitModuleReport;
|
||||
}
|
||||
|
||||
export interface TestReportSummary {
|
||||
hasErrors: boolean;
|
||||
markdownContent: MarkdownString;
|
||||
}
|
||||
|
||||
export function summarizeJunitReport(
|
||||
testReports: TestReport[],
|
||||
options?: { onlyErrors: boolean },
|
||||
): TestReportSummary {
|
||||
const onlyErrors = options?.onlyErrors ?? false;
|
||||
|
||||
const testReportQuickSummaryRows: string[] = [];
|
||||
const testReportDetailsRows: string[] = [];
|
||||
const testReportErrorLogs: string[] = [];
|
||||
let hasErrors = false;
|
||||
|
||||
const mergedReports = mergeSameProjectReports(testReports);
|
||||
for (const report of mergedReports) {
|
||||
const project = report.projectName;
|
||||
const projectReport: JUnitModuleReport = report.projectReport;
|
||||
testReportQuickSummaryRows.push(
|
||||
`| ${escapePipe(report.projectName)} | ${escapePipe(mapStatusToEmoji(projectReport.status))} | ${escapePipe(projectReport.success)} | ${escapePipe(projectReport.skipped)} | ${projectReport.errors + projectReport.failures} |`,
|
||||
);
|
||||
|
||||
for (const testsuite of projectReport.testsuites) {
|
||||
for (const testcase of testsuite.testcases) {
|
||||
const name = testcase.name ?? "";
|
||||
const duration = safeNum(testcase.time);
|
||||
const failed = testcase.status === "failed" || testcase.status === "error";
|
||||
if (failed) hasErrors = true;
|
||||
if (onlyErrors) {
|
||||
// then only print errors, and details like logs
|
||||
if (failed) {
|
||||
const message = testcase.message ?? "";
|
||||
const details = testcase.details ? "\n\n" + testcase.details : "";
|
||||
|
||||
testReportErrorLogs.push(
|
||||
`${escapePipe(project)} > ${escapePipe(testsuite.name)} > ${escapePipe(name)} ${mapStatusToEmoji(testcase.status)} in ${duration}:
|
||||
\n${codeBlock(message + details)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
testReportDetailsRows.push(
|
||||
`| ${escapePipe(project)} | ${escapePipe(testsuite.name)} | ${escapePipe(name)} | ${mapStatusToEmoji(testcase.status)} | ${duration} | ${escapePipe(truncate(testcase.message ?? "", 200))} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let markdownContent = "## Tests report quick summary:";
|
||||
markdownContent =
|
||||
markdownContent +
|
||||
`\n| Project | Status | Success | Skipped | Failed |\n|---|---|---|---|---|`;
|
||||
markdownContent = markdownContent + "\n" + [...testReportQuickSummaryRows].join("\n");
|
||||
if (testReportDetailsRows.length > 0) {
|
||||
markdownContent = markdownContent + "\n\n" + "## Tests report details:";
|
||||
const header = `| Project | Suite | Test | Status | Duration (s) | Message |\n|---|---|---|---|---:|---|`;
|
||||
markdownContent = markdownContent + "\n" + [header, ...testReportDetailsRows].join("\n");
|
||||
}
|
||||
if (testReportErrorLogs.length > 0) {
|
||||
markdownContent = markdownContent + "\n## Failed tests:";
|
||||
markdownContent = markdownContent + "\n" + [...testReportErrorLogs].join("\n");
|
||||
}
|
||||
|
||||
return { hasErrors, markdownContent };
|
||||
|
||||
// merge reports that share the same projectName by concatenating testsuites
|
||||
function mergeSameProjectReports(reports: TestReport[]): TestReport[] {
|
||||
const byProject = new Map<string, JUnitModuleReport>();
|
||||
|
||||
for (const r of reports) {
|
||||
const key = r.projectName;
|
||||
const existing = byProject.get(key);
|
||||
if (!existing) {
|
||||
// clone a shallow copy so we don't mutate the original
|
||||
const cloned: JUnitModuleReport = {
|
||||
...r.projectReport,
|
||||
testsuites: [...r.projectReport.testsuites],
|
||||
} as JUnitModuleReport;
|
||||
computeModuleAggregates(cloned);
|
||||
byProject.set(key, cloned);
|
||||
} else {
|
||||
// concatenate testsuites and recompute aggregates
|
||||
existing.testsuites = [...existing.testsuites, ...r.projectReport.testsuites];
|
||||
computeModuleAggregates(existing);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild TestReport array
|
||||
return Array.from(byProject.entries()).map(([projectName, projectReport]) => ({
|
||||
projectName,
|
||||
projectReport,
|
||||
}));
|
||||
}
|
||||
|
||||
// recompute success/skip/error/failure counts and overall status from testcases
|
||||
function computeModuleAggregates(moduleReport: JUnitModuleReport): void {
|
||||
let success = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
let failures = 0;
|
||||
|
||||
for (const suite of moduleReport.testsuites) {
|
||||
for (const tc of suite.testcases) {
|
||||
switch (tc.status) {
|
||||
case "success":
|
||||
success++; break;
|
||||
case "skipped":
|
||||
skipped++; break;
|
||||
case "error":
|
||||
errors++; break;
|
||||
case "failed":
|
||||
failures++; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = success + skipped + errors + failures;
|
||||
// update known aggregate fields if present on the type
|
||||
moduleReport.success = success;
|
||||
moduleReport.skipped = skipped;
|
||||
moduleReport.errors = errors;
|
||||
moduleReport.failures = failures;
|
||||
if ("tests" in moduleReport) {
|
||||
moduleReport.tests = total;
|
||||
}
|
||||
|
||||
// status rules: all skipped => skipped; any error => error; any failed => failed; else success
|
||||
let status: "success" | "failed" | "error" | "skipped";
|
||||
if (total > 0 && skipped === total) status = "skipped";
|
||||
else if (errors > 0) status = "error";
|
||||
else if (failures > 0) status = "failed";
|
||||
else status = "success";
|
||||
moduleReport.status = status;
|
||||
}
|
||||
|
||||
// helpers scoped below
|
||||
function escapePipe(s: string | number | undefined): string {
|
||||
const str = s == null ? "" : String(s);
|
||||
// escape pipe and newlines for markdown table cells
|
||||
return str.replace(/\|/g, "\\|").replace(/\r?\n/g, " ↵ ");
|
||||
}
|
||||
|
||||
function codeBlock(s: string | number | undefined): string {
|
||||
const str = s == null ? "" : String(s);
|
||||
return `\`\`\`\n${str}\n\`\`\`\n`;
|
||||
}
|
||||
|
||||
function truncate(s: string, max: number): string {
|
||||
return s && s.length > max ? s.slice(0, max - 1) + "…" : s || "";
|
||||
}
|
||||
|
||||
function safeNum(v: number | undefined): string {
|
||||
if (v === undefined || v === null) return "";
|
||||
const n = typeof v === "number" ? v : Number(String(v));
|
||||
if (Number.isFinite(n)) return n.toFixed(3).replace(/\.000$/, "");
|
||||
return String(v);
|
||||
}
|
||||
|
||||
function mapStatusToEmoji(status: "success" | "failed" | "error" | "skipped"): string {
|
||||
switch (status) {
|
||||
case "failed":
|
||||
return "failed ❌";
|
||||
case "error":
|
||||
return "error ❌";
|
||||
case "skipped":
|
||||
return "skipped ⏭️";
|
||||
case "success":
|
||||
return "success ✅";
|
||||
default:
|
||||
throw new Error("Unhandled case");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {WorkingDir} from "../utilities/working-dir";
|
||||
import {MarkdownString, summarizeJunitReport, TestReport,} from "./functions/summarize-junit-report";
|
||||
import {parseJunitModuleReport} from "./functions/parse-junit-module-report";
|
||||
import fg from "fast-glob";
|
||||
import fs from "fs";
|
||||
import {getJavaProjectNameFromBuildAbsolutePath} from "./functions/file-path-utils";
|
||||
|
||||
/**
|
||||
* parse files located at 'testReportsLocationPattern' and generate a summary in Markdown
|
||||
* @param workingDir
|
||||
* @param options
|
||||
*/
|
||||
export async function generateTestReportSummary(
|
||||
workingDir: WorkingDir,
|
||||
options?: {
|
||||
onlyErrors?: boolean;
|
||||
testReportsLocationPattern?: "**/build/test-results/junit/*.xml";
|
||||
},
|
||||
): Promise<MarkdownString> {
|
||||
const onlyErrors = options?.onlyErrors ?? false;
|
||||
const pattern = options?.testReportsLocationPattern ?? "**/build/test-results/junit/*.xml";
|
||||
|
||||
// Find matching report files under the provided working directory
|
||||
const junitXmlReportsFilenames = await fg.async(pattern, {
|
||||
cwd: workingDir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
followSymbolicLinks: true,
|
||||
});
|
||||
|
||||
// Parse each JUnit report into a module-level structure
|
||||
const moduleReports: TestReport[] = junitXmlReportsFilenames.map((file) => {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
return {
|
||||
projectName: getJavaProjectNameFromBuildAbsolutePath(file),
|
||||
projectReport: parseJunitModuleReport(content),
|
||||
};
|
||||
});
|
||||
|
||||
// Summarize all parsed reports into a single Markdown string
|
||||
return summarizeJunitReport(moduleReports, {onlyErrors: onlyErrors}).markdownContent;
|
||||
}
|
||||
30
dev-tools/kestra-devtools/src/utilities/working-dir.ts
Normal file
30
dev-tools/kestra-devtools/src/utilities/working-dir.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export type WorkingDir = string;
|
||||
|
||||
/**
|
||||
* helper to handle working dir passed in CLI
|
||||
* @param workingDir by default the repository root
|
||||
*/
|
||||
export function getWorkingDir(workingDir?: string): WorkingDir {
|
||||
if (!workingDir) {
|
||||
throw new Error(
|
||||
"an absolute working dir is for required, this can be improved for better DX",
|
||||
);
|
||||
}
|
||||
if (!path.isAbsolute(workingDir)) {
|
||||
throw new Error(`Working directory must be an absolute path: ${workingDir}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workingDir)) {
|
||||
throw new Error(`Working directory does not exist: ${workingDir}`);
|
||||
}
|
||||
|
||||
const stat = fs.statSync(workingDir);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Working directory is not a directory: ${workingDir}`);
|
||||
}
|
||||
|
||||
return workingDir;
|
||||
}
|
||||
12
dev-tools/kestra-devtools/tsconfig.json
Normal file
12
dev-tools/kestra-devtools/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "vitest"]
|
||||
},
|
||||
"include": ["src", "vite.config.ts", "vitest.config.ts", "eslint.config.js"]
|
||||
}
|
||||
9
dev-tools/kestra-devtools/tsconfig.types.json
Normal file
9
dev-tools/kestra-devtools/tsconfig.types.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
25
dev-tools/kestra-devtools/vite.config.ts
Normal file
25
dev-tools/kestra-devtools/vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { builtinModules } from "node:module";
|
||||
|
||||
// ensure Node-builtins stay external
|
||||
const externals = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)];
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: "node18",
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: "src/kestra-devtools-cli.ts",
|
||||
formats: ["cjs"],
|
||||
fileName: () => "kestra-devtools-cli.cjs",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: externals,
|
||||
output: {
|
||||
// Make the output an executable CLI
|
||||
banner: "#!/usr/bin/env node",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
12
dev-tools/kestra-devtools/vitest.config.ts
Normal file
12
dev-tools/kestra-devtools/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts"],
|
||||
coverage: {
|
||||
reporter: ["text", "html"],
|
||||
reportsDirectory: "coverage",
|
||||
},
|
||||
},
|
||||
});
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "kestra",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -20,8 +20,9 @@ dependencies {
|
||||
def kafkaVersion = "4.1.0"
|
||||
def opensearchVersion = "3.2.0"
|
||||
def opensearchRestVersion = "3.2.0"
|
||||
def flyingSaucerVersion = "9.13.3"
|
||||
def jacksonVersion = "2.19.2"
|
||||
def flyingSaucerVersion = "10.0.0"
|
||||
def jacksonVersion = "2.20.0"
|
||||
def jacksonAnnotationsVersion = "2.20"
|
||||
def jugVersion = "5.1.0"
|
||||
def langchain4jVersion = "1.4.0"
|
||||
def langchain4jCommunityVersion = "1.4.0-beta10"
|
||||
@@ -33,8 +34,8 @@ dependencies {
|
||||
api platform("io.qameta.allure:allure-bom:2.29.1")
|
||||
// we define cloud bom here for GCP, Azure and AWS so they are aligned for all plugins that use them (secret, storage, oss and ee plugins)
|
||||
api platform('com.google.cloud:libraries-bom:26.67.0')
|
||||
api platform("com.azure:azure-sdk-bom:1.2.37")
|
||||
api platform('software.amazon.awssdk:bom:2.33.2')
|
||||
api platform("com.azure:azure-sdk-bom:1.2.38")
|
||||
api platform('software.amazon.awssdk:bom:2.33.5')
|
||||
api platform("dev.langchain4j:langchain4j-bom:$langchain4jVersion")
|
||||
api platform("dev.langchain4j:langchain4j-community-bom:$langchain4jCommunityVersion")
|
||||
|
||||
@@ -51,7 +52,7 @@ dependencies {
|
||||
// ugly hack for jackson: as enforcing platform didn't work (it didn't enforce everywhere, not in plugins), we had to force all jackson libs individually.
|
||||
api("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.core:jackson-annotations:$jacksonAnnotationsVersion")
|
||||
api("com.fasterxml.jackson.module:jackson-module-parameter-names:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion")
|
||||
api("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:$jacksonVersion")
|
||||
@@ -77,12 +78,12 @@ dependencies {
|
||||
api 'software.amazon.awssdk.crt:aws-crt:0.38.11'
|
||||
|
||||
// we need at least 0.14, it could be removed when Micronaut contains a recent only version in their BOM
|
||||
api "io.micrometer:micrometer-core:1.15.3"
|
||||
api "io.micrometer:micrometer-core:1.15.4"
|
||||
// We need at least 6.17, it could be removed when Micronaut contains a recent only version in their BOM
|
||||
api "io.micronaut.openapi:micronaut-openapi-bom:6.17.3"
|
||||
api "io.micronaut.openapi:micronaut-openapi-bom:6.18.0"
|
||||
|
||||
// Other libs
|
||||
api("org.projectlombok:lombok:1.18.38")
|
||||
api("org.projectlombok:lombok:1.18.40")
|
||||
api("org.codehaus.janino:janino:3.1.12")
|
||||
api group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: '2.25.1'
|
||||
api group: 'org.slf4j', name: 'jul-to-slf4j', version: slf4jVersion
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {setup} from "@storybook/vue3-vite";
|
||||
import {withThemeByClassName} from "@storybook/addon-themes";
|
||||
import initApp from "../src/utils/init";
|
||||
import stores from "../src/stores/store";
|
||||
|
||||
import "../src/styles/vendor.scss";
|
||||
import "../src/styles/app.scss";
|
||||
@@ -33,9 +32,14 @@ const preview = {
|
||||
]
|
||||
};
|
||||
|
||||
setup((app) => {
|
||||
initApp(app, [], stores, en);
|
||||
});
|
||||
setup(async (app) => {
|
||||
const {piniaStore} = await initApp(app, [], {}, en);
|
||||
piniaStore.use(({store}) => {
|
||||
store.$http = {
|
||||
get: () => Promise.resolve({data: []}),
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
window.addEventListener("unhandledrejection", (evt) => {
|
||||
|
||||
791
ui/package-lock.json
generated
791
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,22 +24,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.5",
|
||||
"@kestra-io/ui-libs": "^0.0.245",
|
||||
"@kestra-io/ui-libs": "^0.0.249",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.46.2",
|
||||
"@vue-flow/core": "^1.46.3",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.5.0",
|
||||
"core-js": "^3.45.1",
|
||||
"cronstrue": "^3.2.0",
|
||||
"cronstrue": "^3.3.0",
|
||||
"cytoscape": "^3.33.0",
|
||||
"dagre": "^0.8.5",
|
||||
"el-table-infinite-scroll": "^3.0.7",
|
||||
"element-plus": "2.10.5",
|
||||
"element-plus": "2.11.2",
|
||||
"humanize-duration": "^3.33.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -59,7 +59,7 @@
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"pinia": "^3.0.3",
|
||||
"posthog-js": "^1.261.6",
|
||||
"posthog-js": "^1.262.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"semver": "^7.7.2",
|
||||
"shiki": "^3.12.2",
|
||||
@@ -67,15 +67,14 @@
|
||||
"vue": "^3.5.21",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-gtag": "^3.5.2",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-gtag": "^3.6.1",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-material-design-icons": "^5.3.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-sidebar-menu": "^5.7.0",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8",
|
||||
"vue3-popper": "^1.5.0",
|
||||
"vue3-tour": "github:kestra-io/vue3-tour",
|
||||
"vuex": "^4.1.0",
|
||||
"xss": "^1.0.15",
|
||||
"yaml": "^2.8.1"
|
||||
},
|
||||
@@ -86,20 +85,20 @@
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@rushstack/eslint-patch": "^1.12.0",
|
||||
"@shikijs/markdown-it": "^3.12.2",
|
||||
"@storybook/addon-themes": "^9.1.4",
|
||||
"@storybook/addon-vitest": "^9.1.4",
|
||||
"@storybook/addon-themes": "^9.1.5",
|
||||
"@storybook/addon-vitest": "^9.1.5",
|
||||
"@storybook/test-runner": "^0.23.0",
|
||||
"@storybook/vue3-vite": "^9.1.4",
|
||||
"@storybook/vue3-vite": "^9.1.5",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/moment": "^2.11.29",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
"@types/testing-library__user-event": "^4.1.1",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"@typescript-eslint/parser": "^8.43.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
@@ -110,10 +109,10 @@
|
||||
"change-case": "5.4.4",
|
||||
"cross-env": "^10.0.0",
|
||||
"decompress": "^4.2.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-storybook": "^9.1.4",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-storybook": "^9.1.5",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.3.0",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
@@ -123,13 +122,13 @@
|
||||
"playwright": "^1.55.0",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sass": "^1.92.0",
|
||||
"storybook": "^9.1.4",
|
||||
"sass": "^1.92.1",
|
||||
"storybook": "^9.1.5",
|
||||
"storybook-vue3-router": "^6.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"uuid": "^11.1.0",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
@@ -137,9 +136,9 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.9",
|
||||
"@esbuild/darwin-x64": "^0.25.9",
|
||||
"@esbuild/linux-x64": "^0.25.9",
|
||||
"@rollup/rollup-darwin-arm64": "^4.50.0",
|
||||
"@rollup/rollup-darwin-x64": "^4.50.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.50.0",
|
||||
"@rollup/rollup-darwin-arm64": "^4.50.1",
|
||||
"@rollup/rollup-darwin-x64": "^4.50.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.50.1",
|
||||
"@swc/core-darwin-arm64": "^1.13.5",
|
||||
"@swc/core-darwin-x64": "^1.13.5",
|
||||
"@swc/core-linux-x64-gnu": "^1.13.5"
|
||||
|
||||
@@ -3,17 +3,13 @@
|
||||
<template #content>
|
||||
<code>{{ value }}</code>
|
||||
</template>
|
||||
<a href="#">
|
||||
<code :id="uuid" @click="emit('click')" class="text-nowrap" :class="{'link': hasClickListener}">
|
||||
{{ transformValue }}
|
||||
</code>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
<a v-else href="#">
|
||||
<code :id="uuid" class="text-nowrap" @click="onClick">
|
||||
<code :id="uuid" @click="emit('click')" class="text-nowrap" :class="{'link': hasClickListener}">
|
||||
{{ transformValue }}
|
||||
</code>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
<code v-else :id="uuid" class="text-nowrap" @click="onClick">
|
||||
{{ transformValue }}
|
||||
</code>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -64,16 +60,10 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
a code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
code.link {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
code.link {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,145 +1,153 @@
|
||||
<template>
|
||||
<el-splitter class="default-theme" v-bind="$attrs" @resize-end="onResize">
|
||||
<el-splitter-panel
|
||||
v-for="(panel, panelIndex) in panels"
|
||||
min="10%"
|
||||
:key="panelIndex"
|
||||
:size="panelSizes[panelIndex] ?? panel.size"
|
||||
@dragover.prevent="(e:DragEvent) => panelDragOver(e, panelIndex)"
|
||||
@dragleave.prevent="panelDragLeave"
|
||||
@drop.prevent="(e:DragEvent) => panelDrop(e, panelIndex)"
|
||||
:class="{'panel-dragover': panel.dragover}"
|
||||
>
|
||||
<div class="editor-tabs-container">
|
||||
<el-button
|
||||
:icon="DragVertical"
|
||||
link
|
||||
class="tab-icon drag-handle"
|
||||
draggable="true"
|
||||
@dragstart="(e:DragEvent) => panelDragStart(e, panelIndex)"
|
||||
/>
|
||||
<div
|
||||
class="editor-tabs"
|
||||
role="tablist"
|
||||
@dragover.prevent="dragover"
|
||||
@dragleave.prevent="throttle(removeAllPotentialTabs, 300)"
|
||||
@drop="drop"
|
||||
:data-panel-index="panelIndex"
|
||||
:class="{dragover: panel.dragover}"
|
||||
ref="tabContainerRefs"
|
||||
>
|
||||
<template
|
||||
v-for="tab in panel.tabs"
|
||||
:key="tab.value"
|
||||
>
|
||||
<button
|
||||
v-if="!tab.potential"
|
||||
class="editor-tab"
|
||||
role="tab"
|
||||
:class="{active: tab.value === panel.activeTab?.value}"
|
||||
draggable="true"
|
||||
@dragstart="(e) => {
|
||||
if(e.dataTransfer){
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
dragstart(panelIndex, tab.value);
|
||||
}"
|
||||
@dragleave.prevent
|
||||
:data-tab-id="tab.value"
|
||||
@click="handleTabClick(panel, tab)"
|
||||
@mouseup="middleMouseClose($event, panelIndex, tab)"
|
||||
>
|
||||
<component :is="tab.button.icon" class="tab-icon" />
|
||||
{{ tab.button.label }}
|
||||
<CircleMediumIcon v-if="tab.dirty" class="dirty-icon" />
|
||||
<CloseIcon @click.stop="destroyTab(panelIndex, tab)" class="tab-icon" />
|
||||
</button>
|
||||
<div v-else class="potential-container">
|
||||
<div class="potential" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="buttons-container">
|
||||
<button
|
||||
v-if="panel.tabs.filter(t => !t.potential).length > 1"
|
||||
@click="splitPanel(panelIndex)"
|
||||
class="split_right"
|
||||
title="Split panel"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M22.038 20.5599C22.0402 21.35 21.4014 21.9924 20.6112 21.9946L3.47196 22.0424C2.6818 22.0446 2.03946 21.4058 2.03725 20.6157L1.98939 3.45824C1.98718 2.66808 2.62595 2.02574 3.41611 2.02353L20.5554 1.97571C21.3455 1.97351 21.9879 2.61228 21.9901 3.40244L22.038 20.5599ZM20.626 20.5807L10.5998 20.6086L10.5517 3.37297L20.5779 3.345L20.626 20.5807ZM9.10343 20.611L3.38734 20.6269L3.33925 3.39126L9.05535 3.37531L9.10343 20.611Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<el-button :icon="DotsVertical" link class="me-2 tab-icon" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="m-2">
|
||||
<el-dropdown-item
|
||||
:icon="DockRight"
|
||||
:disabled="panelIndex === panels.length - 1"
|
||||
@click="movePanel(panelIndex, 'right')"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.move_right") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:icon="DockLeft"
|
||||
:disabled="panelIndex === 0"
|
||||
@click="movePanel(panelIndex, 'left')"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.move_left") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Close" @click="closeAllTabs(panelIndex)">
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.close_all_tabs") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="panel.activeTab?.value === 'code'"
|
||||
:icon="Keyboard"
|
||||
@click="showKeyShortcuts()"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("editor_shortcuts.label") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content-panel"
|
||||
:data-panel-index="panelIndex"
|
||||
@drop="drop"
|
||||
@dragover.prevent="dragover"
|
||||
@dragleave.prevent="removeAllPotentialTabs"
|
||||
@dragenter.prevent
|
||||
<Empty v-if="!panels.length" type="panels" />
|
||||
<template v-else>
|
||||
<el-splitter-panel
|
||||
v-for="(panel, panelIndex) in panels"
|
||||
min="10%"
|
||||
:key="panelIndex"
|
||||
:size="panelSizes[panelIndex] ?? panel.size"
|
||||
@dragover.prevent="(e:DragEvent) => panelDragOver(e, panelIndex)"
|
||||
@dragleave.prevent="panelDragLeave"
|
||||
@drop.prevent="(e:DragEvent) => panelDrop(e, panelIndex)"
|
||||
:class="{'panel-dragover': panel.dragover}"
|
||||
>
|
||||
<KeepAlive v-if="panel.activeTab">
|
||||
<component
|
||||
:key="panel.activeTab.value"
|
||||
:is="panel.activeTab.component"
|
||||
:panelIndex="panelIndex"
|
||||
:tabIndex="panel.tabs.findIndex(t => t.value === panel.activeTab.value)"
|
||||
<div class="editor-tabs-container">
|
||||
<el-button
|
||||
:icon="DragVertical"
|
||||
link
|
||||
class="tab-icon drag-handle"
|
||||
draggable="true"
|
||||
@dragstart="(e:DragEvent) => panelDragStart(e, panelIndex)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<div
|
||||
class="editor-tabs"
|
||||
role="tablist"
|
||||
@dragover.prevent="dragover"
|
||||
@dragleave.prevent="throttle(removeAllPotentialTabs, 300)"
|
||||
@drop="drop"
|
||||
:data-panel-index="panelIndex"
|
||||
:class="{dragover: panel.dragover}"
|
||||
ref="tabContainerRefs"
|
||||
>
|
||||
<template
|
||||
v-for="tab in panel.tabs"
|
||||
:key="tab.value"
|
||||
>
|
||||
<button
|
||||
v-if="!tab.potential"
|
||||
class="editor-tab"
|
||||
role="tab"
|
||||
:class="{active: tab.value === panel.activeTab?.value}"
|
||||
draggable="true"
|
||||
@dragstart="(e) => {
|
||||
if(e.dataTransfer){
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
dragstart(panelIndex, tab.value);
|
||||
}"
|
||||
@dragleave.prevent
|
||||
:data-tab-id="tab.value"
|
||||
@click="handleTabClick(panel, tab)"
|
||||
@mouseup="middleMouseClose($event, panelIndex, tab)"
|
||||
>
|
||||
<component :is="tab.button.icon" class="tab-icon" />
|
||||
{{ tab.button.label }}
|
||||
<CircleMediumIcon v-if="tab.dirty" class="dirty-icon" />
|
||||
<CloseIcon @click.stop="destroyTab(panelIndex, tab)" class="tab-icon" />
|
||||
</button>
|
||||
<div v-else class="potential-container">
|
||||
<div class="potential" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="buttons-container">
|
||||
<button
|
||||
v-if="panel.tabs.filter(t => !t.potential).length > 1"
|
||||
@click="splitPanel(panelIndex)"
|
||||
class="split_right"
|
||||
title="Split panel"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M22.038 20.5599C22.0402 21.35 21.4014 21.9924 20.6112 21.9946L3.47196 22.0424C2.6818 22.0446 2.03946 21.4058 2.03725 20.6157L1.98939 3.45824C1.98718 2.66808 2.62595 2.02574 3.41611 2.02353L20.5554 1.97571C21.3455 1.97351 21.9879 2.61228 21.9901 3.40244L22.038 20.5599ZM20.626 20.5807L10.5998 20.6086L10.5517 3.37297L20.5779 3.345L20.626 20.5807ZM9.10343 20.611L3.38734 20.6269L3.33925 3.39126L9.05535 3.37531L9.10343 20.611Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<el-button :icon="DotsVertical" link class="me-2 tab-icon" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="m-2">
|
||||
<el-dropdown-item
|
||||
:icon="DockRight"
|
||||
:disabled="panelIndex === panels.length - 1"
|
||||
@click="movePanel(panelIndex, 'right')"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.move_right") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:icon="DockLeft"
|
||||
:disabled="panelIndex === 0"
|
||||
@click="movePanel(panelIndex, 'left')"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.move_left") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="panel.tabs.length > 1" :icon="Close" @click="closeAllTabs(panelIndex)">
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.close_all_tabs") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="Close" @click="closeAllPanels()">
|
||||
<span class="small-text">
|
||||
{{ t("multi_panel_editor.close_all_panels") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="panel.activeTab?.value === 'code'"
|
||||
:icon="Keyboard"
|
||||
@click="showKeyShortcuts()"
|
||||
>
|
||||
<span class="small-text">
|
||||
{{ t("editor_shortcuts.label") }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="dragging"
|
||||
class="editor-content-overlay"
|
||||
:class="{dragover: panel.dragover}"
|
||||
/>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
class="content-panel"
|
||||
:data-panel-index="panelIndex"
|
||||
@drop="drop"
|
||||
@dragover.prevent="dragover"
|
||||
@dragleave.prevent="removeAllPotentialTabs"
|
||||
@dragenter.prevent
|
||||
>
|
||||
<KeepAlive v-if="panel.activeTab">
|
||||
<component
|
||||
:key="panel.activeTab.value"
|
||||
:is="panel.activeTab.component"
|
||||
:panelIndex="panelIndex"
|
||||
:tabIndex="panel.tabs.findIndex(t => t.value === panel.activeTab.value)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<div
|
||||
v-if="dragging"
|
||||
class="editor-content-overlay"
|
||||
:class="{dragover: panel.dragover}"
|
||||
/>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</template>
|
||||
</el-splitter>
|
||||
|
||||
<div
|
||||
@@ -172,6 +180,8 @@
|
||||
import {CODE_PREFIX} from "./flows/useCodePanels";
|
||||
import {useKeyShortcuts} from "../utils/useKeyShortcuts";
|
||||
|
||||
import Empty from "./layout/empty/Empty.vue";
|
||||
|
||||
import CloseIcon from "vue-material-design-icons/Close.vue"
|
||||
import CircleMediumIcon from "vue-material-design-icons/CircleMedium.vue"
|
||||
import DragVertical from "vue-material-design-icons/DragVertical.vue";
|
||||
@@ -180,6 +190,7 @@
|
||||
import DockRight from "vue-material-design-icons/DockRight.vue";
|
||||
import Close from "vue-material-design-icons/Close.vue";
|
||||
import Keyboard from "vue-material-design-icons/Keyboard.vue";
|
||||
|
||||
import {useEditorStore} from "../stores/editor";
|
||||
import {trackTabOpen, trackTabClose} from "../utils/tabTracking";
|
||||
|
||||
@@ -518,6 +529,10 @@
|
||||
panels.value[panelIndex].tabs = [];
|
||||
}
|
||||
|
||||
function closeAllPanels(){
|
||||
panels.value = [];
|
||||
}
|
||||
|
||||
function destroyTab(panelIndex:number, tab: Tab){
|
||||
trackTabClose(tab);
|
||||
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
} else {
|
||||
return {
|
||||
name: this.routeName || this.$route.name,
|
||||
params: {...this.$route.params, ...{tab: tab.name}},
|
||||
query: {...(tab.query || {})}
|
||||
params: {...this.$route.params, tab: tab.name},
|
||||
query: {...tab.query}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -128,12 +128,22 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="visibleColumns.date" :label="$t('date')">
|
||||
<el-table-column v-if="visibleColumns.date">
|
||||
<template #header>
|
||||
<el-tooltip :content="$t('last trigger date tooltip')" placement="top" popperClass="wide-tooltip">
|
||||
<span>{{ $t('last trigger date') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<DateAgo :inverted="true" :date="scope.row.date" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="visibleColumns.updatedDate" :label="$t('updated date')">
|
||||
<el-table-column v-if="visibleColumns.updatedDate">
|
||||
<template #header>
|
||||
<el-tooltip :content="$t('context updated date tooltip')" placement="top" popperClass="wide-tooltip">
|
||||
<span>{{ $t('context updated date') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<DateAgo :inverted="true" :date="scope.row.updatedDate" />
|
||||
</template>
|
||||
@@ -143,8 +153,12 @@
|
||||
prop="nextExecutionDate"
|
||||
sortable="custom"
|
||||
:sortOrders="['ascending', 'descending']"
|
||||
:label="$t('next execution date')"
|
||||
>
|
||||
<template #header>
|
||||
<el-tooltip :content="$t('next evaluation date tooltip')" placement="top" popperClass="wide-tooltip">
|
||||
<span>{{ $t('next evaluation date') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<DateAgo :inverted="true" :date="scope.row.nextExecutionDate" />
|
||||
</template>
|
||||
@@ -528,16 +542,16 @@
|
||||
},
|
||||
visibleColumns() {
|
||||
const columns = [
|
||||
{prop: "triggerId", label: this.$t("id")},
|
||||
{prop: "flowId", label: this.$t("flow")},
|
||||
{prop: "namespace", label: this.$t("namespace")},
|
||||
{prop: "executionId", label: this.$t("current execution")},
|
||||
{prop: "executionCurrentState", label: this.$t("state")},
|
||||
{prop: "workerId", label: this.$t("workerId")},
|
||||
{prop: "date", label: this.$t("date")},
|
||||
{prop: "updatedDate", label: this.$t("updated date")},
|
||||
{prop: "nextExecutionDate", label: this.$t("next execution date")},
|
||||
{prop: "evaluateRunningDate", label: this.$t("evaluation lock date")},
|
||||
{prop: "triggerId"},
|
||||
{prop: "flowId"},
|
||||
{prop: "namespace"},
|
||||
{prop: "executionId"},
|
||||
{prop: "executionCurrentState"},
|
||||
{prop: "workerId"},
|
||||
{prop: "date"},
|
||||
{prop: "updatedDate"},
|
||||
{prop: "nextExecutionDate"},
|
||||
{prop: "evaluateRunningDate"},
|
||||
];
|
||||
|
||||
return columns.reduce((acc, column) => {
|
||||
@@ -545,9 +559,6 @@
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
triggerStore() {
|
||||
return useTriggerStore();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -599,4 +610,12 @@
|
||||
color: var(--ks-content-link);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.wide-tooltip {
|
||||
max-width: 400px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
color: var(--ks-content-primary) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -17,6 +17,7 @@
|
||||
v-model="prompt"
|
||||
@keydown.exact.ctrl.enter="$event.preventDefault(); prompt += '\n'"
|
||||
@keydown.exact.enter.prevent="submitPrompt"
|
||||
class="ai-copilot-placeholder"
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- eslint-disable-next-line vue/no-v-text-v-html-on-component -->
|
||||
@@ -163,4 +164,9 @@
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-copilot-placeholder :deep(textarea::placeholder) {
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -68,7 +68,6 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed} from "vue"
|
||||
import {useRouter, useRoute} from "vue-router"
|
||||
import {useStore} from "vuex"
|
||||
import {useI18n} from "vue-i18n"
|
||||
import {ElMessage} from "element-plus"
|
||||
import type {FormInstance} from "element-plus"
|
||||
@@ -91,7 +90,6 @@
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
const {t} = useI18n()
|
||||
const coreStore = useCoreStore()
|
||||
const miscStore = useMiscStore()
|
||||
@@ -115,7 +113,7 @@
|
||||
const validateCredentials = async (auth: string) => {
|
||||
try {
|
||||
document.cookie = `BASIC_AUTH=${auth};path=/;samesite=strict`;
|
||||
await axios.get(`${apiUrl(store)}/usages/all`, {
|
||||
await axios.get(`${apiUrl()}/usages/all`, {
|
||||
timeout: 10000,
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ export function useChartGenerator(props: {chart: Chart; filters: string[]; showD
|
||||
const data = ref();
|
||||
const generate = async (id: string, pagination?: { pageNumber: number; pageSize: number }) => {
|
||||
const filters = props.filters.concat(decodeSearchParams(route.query, undefined, []) ?? []);
|
||||
const parameters: Parameters = {...(pagination ?? {}), filters: (filters ?? {})};
|
||||
const parameters: Parameters = {...pagination, filters: (filters ?? {})};
|
||||
|
||||
if (!props.showDefault) {
|
||||
data.value = await dashboardStore.generate(id, props.chart.id, parameters);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<Empty v-if="!loading && !getElements().length" :type="`dependencies.${SUBTYPE}`" />
|
||||
<Empty v-if="!isLoading && !getElements().length" :type="`dependencies.${SUBTYPE}`" />
|
||||
<el-splitter v-else class="dependencies">
|
||||
<el-splitter-panel id="graph" v-bind="PANEL">
|
||||
<div v-loading="loading" ref="container" />
|
||||
<div v-loading="isRendering" ref="container" />
|
||||
|
||||
<div class="controls">
|
||||
<el-button
|
||||
@@ -74,7 +74,7 @@
|
||||
const initialNodeID: string = SUBTYPE === FLOW || SUBTYPE === NAMESPACE ? String(route.params.id) : String(route.params.flowId);
|
||||
const TESTING = false; // When true, bypasses API data fetching and uses mock/test data.
|
||||
|
||||
const {getElements, loading, selectedNodeID, selectNode, handlers} = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING);
|
||||
const {getElements, isLoading, isRendering, selectedNodeID, selectNode, handlers} = useDependencies(container, SUBTYPE, initialNodeID, route.params, TESTING);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -260,7 +260,7 @@ function hoverHandler(cy: cytoscape.Core): void {
|
||||
* @param initialNodeID - Optional ID of the node to preselect after layout completes.
|
||||
* @param params - Vue Router params, expected to include `id` and `namespace`.
|
||||
* @param isTesting - When true, bypasses API data fetching and uses mock/test data.
|
||||
* @returns An object with element getters, loading state, selected node ID,
|
||||
* @returns An object with element getters, loading state, rendering state, selected node ID,
|
||||
* selection helpers, and control handlers.
|
||||
*/
|
||||
export function useDependencies(container: Ref<HTMLElement | null>, subtype: typeof FLOW | typeof EXECUTION | typeof NAMESPACE = FLOW, initialNodeID: string, params: RouteParams, isTesting = false) {
|
||||
@@ -281,7 +281,8 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
|
||||
|
||||
let cy: cytoscape.Core;
|
||||
|
||||
const loading = ref(true);
|
||||
const isLoading = ref(true);
|
||||
const isRendering = ref(true);
|
||||
|
||||
const selectedNodeID: Ref<Node["id"] | undefined> = ref(undefined);
|
||||
|
||||
@@ -304,15 +305,23 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
|
||||
onMounted(async () => {
|
||||
if (!container.value) return;
|
||||
|
||||
if (isTesting) elements.value = {data: getDependencies({subtype}), count: getRandomNumber(1, 100)};
|
||||
if (isTesting) {
|
||||
elements.value = {data: getDependencies({subtype}), count: getRandomNumber(1, 100)};
|
||||
|
||||
isLoading.value = false;
|
||||
}
|
||||
else {
|
||||
if (subtype === NAMESPACE) {
|
||||
const {data} = await namespacesStore.loadDependencies({namespace: params.id as string});
|
||||
const nodes = data.nodes ?? [];
|
||||
elements.value = {data: transformResponse(data, NAMESPACE), count: new Set(nodes.map((r: { uid: string }) => r.uid)).size};
|
||||
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
const result = await flowStore.loadDependencies({id: (subtype === FLOW ? params.id : params.flowId) as string, namespace: params.namespace as string, subtype});
|
||||
elements.value = {data: result.data ?? [], count: result.count};
|
||||
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,10 +357,9 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
|
||||
selectHandler(cy, node, selectedNodeID, subtype);
|
||||
});
|
||||
|
||||
cy.on("layoutstop", () => {
|
||||
loading.value = false;
|
||||
|
||||
cy.on("layoutstop", () => {
|
||||
// Reveal nodes after layout rendering completes
|
||||
isRendering.value = false;
|
||||
cy.nodes().style("display", "element");
|
||||
|
||||
const node = isTesting ? cy.nodes()[0] : cy.nodes().filter((n) => n.data("flow") === initialNodeID);
|
||||
@@ -420,7 +428,8 @@ export function useDependencies(container: Ref<HTMLElement | null>, subtype: typ
|
||||
|
||||
return {
|
||||
getElements: () => elements.value.data,
|
||||
loading,
|
||||
isLoading,
|
||||
isRendering,
|
||||
selectedNodeID,
|
||||
selectNode,
|
||||
handlers: {
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: $spacer;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
import action from "../../models/action";
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
import Status from "../../components/Status.vue";
|
||||
import ExecutionUtils from "../../utils/executionUtils";
|
||||
import * as ExecutionUtils from "../../utils/executionUtils";
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
|
||||
export default {
|
||||
@@ -107,7 +107,7 @@
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.id === this.execution.id) {
|
||||
return ExecutionUtils.waitForState(this.$http, this.$store, response.data);
|
||||
return ExecutionUtils.waitForState(this.$http, response.data);
|
||||
} else {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
import action from "../../models/action";
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
import Status from "../../components/Status.vue";
|
||||
import ExecutionUtils from "../../utils/executionUtils";
|
||||
import * as ExecutionUtils from "../../utils/executionUtils";
|
||||
import {shallowRef} from "vue";
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.id === this.execution.id) {
|
||||
return ExecutionUtils.waitForState(this.$http, this.$store, response.data);
|
||||
return ExecutionUtils.waitForState(this.$http, response.data);
|
||||
} else {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
return this.executionsStore.execution;
|
||||
},
|
||||
finalApiUrl() {
|
||||
return apiUrl(this.$store);
|
||||
return apiUrl();
|
||||
},
|
||||
canDelete() {
|
||||
return this.execution && this.authStore.user?.isAllowed(permission.EXECUTION, action.DELETE, this.execution.namespace);
|
||||
|
||||
@@ -163,11 +163,19 @@
|
||||
:label="$t('id')"
|
||||
>
|
||||
<template #default="scope">
|
||||
<Id
|
||||
:value="scope.row.id"
|
||||
:shrink="true"
|
||||
@click="onRowDoubleClick(executionParams(scope.row))"
|
||||
/>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'executions/update',
|
||||
params: {
|
||||
namespace: scope.row.namespace,
|
||||
flowId: scope.row.flowId,
|
||||
id: scope.row.id
|
||||
}
|
||||
}"
|
||||
class="execution-id"
|
||||
>
|
||||
<Id :value="scope.row.id" :shrink="true" />
|
||||
</RouterLink>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -482,6 +490,8 @@
|
||||
import {useAuthStore} from "override/stores/auth.ts";
|
||||
import {useFlowStore} from "../../stores/flow.ts";
|
||||
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
export default {
|
||||
mixins: [RouteContext, RestoreUrl, DataTableActions, SelectTableActions],
|
||||
components: {
|
||||
@@ -705,15 +715,12 @@
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, _, next) {
|
||||
const defaultNamespace = localStorage.getItem(
|
||||
storageKeys.DEFAULT_NAMESPACE,
|
||||
);
|
||||
const query = {...to.query};
|
||||
let queryHasChanged = false;
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (this?.namespace === undefined && defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace;
|
||||
if (this?.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
@@ -1125,16 +1132,8 @@
|
||||
.code-text {
|
||||
color: var(--ks-content-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-message-box {
|
||||
padding: 2rem;
|
||||
max-width: initial;
|
||||
width: 500px;
|
||||
|
||||
.custom-warning {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
:deep(a.execution-id) code {
|
||||
color: var(--bs-code-color) !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {useRouter, useRoute} from "vue-router";
|
||||
// @ts-expect-error no types yet
|
||||
import {inputsToFormData} from "../../utils/submitTask";
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import ExecutionUtils from "../../utils/executionUtils";
|
||||
import * as ExecutionUtils from "../../utils/executionUtils";
|
||||
// @ts-expect-error no types yet
|
||||
import FlowRun from "../../components/flows/FlowRun.vue";
|
||||
import PlayBoxMultiple from "vue-material-design-icons/PlayBoxMultiple.vue";
|
||||
import {useAxios} from "../../utils/axios";
|
||||
|
||||
const {t} = useI18n();
|
||||
const toast = useToast();
|
||||
@@ -38,9 +41,11 @@
|
||||
|
||||
const flow = computed(() => executionsStore.flow);
|
||||
|
||||
const axios = useAxios()
|
||||
|
||||
const handleReplaySubmit = async ({inputs}: any) => {
|
||||
|
||||
const formData = inputsToFormData({$http: null, $store: null}, flow.value.inputs, inputs);
|
||||
|
||||
const formData = inputsToFormData({}, flow.value.inputs, inputs);
|
||||
let response = await executionsStore.replayExecutionWithInputs({
|
||||
executionId: props.execution.id,
|
||||
taskRunId: props.taskRun?.id,
|
||||
@@ -49,7 +54,7 @@
|
||||
});
|
||||
|
||||
if (response.data.id === props.execution.id) {
|
||||
response = await ExecutionUtils.waitForState(null, null, response.data);
|
||||
response = await ExecutionUtils.waitForState(axios, response.data) as any;
|
||||
}
|
||||
|
||||
const execution = response.data;
|
||||
|
||||
@@ -91,13 +91,12 @@
|
||||
import {useI18n} from "vue-i18n"
|
||||
import {useToast} from "../../utils/toast"
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
import {useStore} from "vuex"
|
||||
import {useFlowStore} from "../../stores/flow"
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
import {useExecutionsStore} from "../../stores/executions"
|
||||
import action from "../../models/action"
|
||||
import permission from "../../models/permission"
|
||||
import ExecutionUtils from "../../utils/executionUtils"
|
||||
import * as ExecutionUtils from "../../utils/executionUtils"
|
||||
import ReplayWithInputs from "./ReplayWithInputs.vue"
|
||||
import RestartIcon from "vue-material-design-icons/Restart.vue"
|
||||
import PlayBoxMultiple from "vue-material-design-icons/PlayBoxMultiple.vue"
|
||||
@@ -115,7 +114,6 @@
|
||||
const emit = defineEmits(["follow"])
|
||||
|
||||
const {t} = useI18n()
|
||||
const store = useStore()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const flowStore = useFlowStore()
|
||||
@@ -214,7 +212,7 @@
|
||||
})
|
||||
|
||||
const execution = response.data.id === props.execution.id && $http
|
||||
? await ExecutionUtils.waitForState($http, store, response.data)
|
||||
? await ExecutionUtils.waitForState($http, response.data)
|
||||
: response.data
|
||||
|
||||
executionsStore.execution = execution
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import action from "../../models/action";
|
||||
import {State} from "@kestra-io/ui-libs"
|
||||
import FlowUtils from "../../utils/flowUtils";
|
||||
import ExecutionUtils from "../../utils/executionUtils";
|
||||
import * as ExecutionUtils from "../../utils/executionUtils";
|
||||
import InputsForm from "../../components/inputs/InputsForm.vue";
|
||||
import {inputsToFormData} from "../../utils/submitTask";
|
||||
import {mapStores} from "pinia";
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
}
|
||||
},
|
||||
itemUrl(value) {
|
||||
return `${apiUrl(this.$store)}/executions/${this.execution.id}/file?path=${encodeURI(value)}`;
|
||||
return `${apiUrl()}/executions/${this.execution.id}/file?path=${encodeURI(value)}`;
|
||||
},
|
||||
getFileSize(){
|
||||
if (this.isFile(this.value)) {
|
||||
this.$http(`${apiUrl(this.$store)}/executions/${this?.execution?.id}/file/metas?path=${this.value}`, {
|
||||
this.$http(`${apiUrl()}/executions/${this?.execution?.id}/file/metas?path=${this.value}`, {
|
||||
validateStatus: (status) => status === 200 || status === 404 || status === 422
|
||||
}).then(r => this.humanSize = Utils.humanFileSize(r.data.size))
|
||||
}
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, shallowRef, onMounted, watch} from "vue";
|
||||
import {ElTree} from "element-plus";
|
||||
import {useStore} from "vuex";
|
||||
import {useExecutionsStore} from "../../../stores/executions";
|
||||
import {usePluginsStore} from "../../../stores/plugins";
|
||||
|
||||
@@ -166,22 +165,22 @@
|
||||
import SubFlowLink from "../../flows/SubFlowLink.vue";
|
||||
import TimelineTextOutline from "vue-material-design-icons/TimelineTextOutline.vue";
|
||||
import TextBoxSearchOutline from "vue-material-design-icons/TextBoxSearchOutline.vue";
|
||||
import {useAxios} from "../../../utils/axios";
|
||||
|
||||
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<string>("");
|
||||
|
||||
|
||||
const computedDebugValue = computed(() => {
|
||||
const formatTask = (task) => {
|
||||
const formatTask = (task: string) => {
|
||||
if (!task) return "";
|
||||
return task.includes("-") ? `["${task}"]` : `.${task}`;
|
||||
};
|
||||
|
||||
const formatPath = (path) => {
|
||||
const formatPath = (path: string) => {
|
||||
if (!path.includes("-")) return `.${path}`;
|
||||
|
||||
const bracketIndex = path.indexOf("[");
|
||||
@@ -210,13 +209,15 @@
|
||||
const taskRunList = [...execution.value?.taskRunList ?? []];
|
||||
return taskRunList.find((e) => e.taskId === filter);
|
||||
};
|
||||
|
||||
const axios = useAxios();
|
||||
const onDebugExpression = (expression: string) => {
|
||||
const taskRun = selectedTask();
|
||||
|
||||
if (!taskRun) return;
|
||||
|
||||
const URL = `${apiUrl(store)}/executions/${taskRun?.executionId}/eval/${taskRun.id}`;
|
||||
store.$http
|
||||
const URL = `${apiUrl()}/executions/${taskRun?.executionId}/eval/${taskRun.id}`;
|
||||
axios
|
||||
.post(URL, expression, {headers: {"Content-type": "text/plain"}})
|
||||
.then((response) => {
|
||||
try {
|
||||
@@ -290,7 +291,7 @@
|
||||
return {label: trim(data.value), regular: true};
|
||||
};
|
||||
|
||||
const expandedValue = ref([]);
|
||||
const expandedValue = ref("");
|
||||
const selected = ref<string[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
let queryEntries = filters.flatMap(({key: key, comparator: comparator, value: value}) => {
|
||||
let queryEntries = filters.flatMap(({key, comparator, value}) => {
|
||||
let queryKey = reversedQueryRemapper?.[key] ?? key;
|
||||
|
||||
if (!props.legacyQuery) {
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
.filter((label) => label.key !== null && label.value !== null && label.key !== "" && label.value !== "")
|
||||
.map((label) => this.generateExecutionLabel(label.key, label.value));
|
||||
|
||||
const origin = baseUrl ? apiUrl(this.$store) : `${location.origin}${basePath(this.$store)}`;
|
||||
const origin = baseUrl ? apiUrl() : `${location.origin}${basePath()}`;
|
||||
|
||||
var url = `${origin}/executions/${this.flow.namespace}/${this.flow.id}`;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import RouteContext from "../../mixins/routeContext";
|
||||
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
||||
import MultiPanelFlowEditorView from "./MultiPanelFlowEditorView.vue";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {useBlueprintsStore} from "../../stores/blueprints";
|
||||
import {useCoreStore} from "../../stores/core";
|
||||
import {editorViewTypes} from "../../utils/constants";
|
||||
@@ -19,6 +18,7 @@
|
||||
import {getRandomID} from "../../../scripts/id";
|
||||
import {useEditorStore} from "../../stores/editor";
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
export default {
|
||||
mixins: [RouteContext],
|
||||
@@ -50,8 +50,7 @@
|
||||
} else if (blueprintId && blueprintSource) {
|
||||
flowYaml = await this.blueprintsStore.getBlueprintSource({type: blueprintSource, kind: "flow", id: blueprintId});
|
||||
} else {
|
||||
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
|
||||
const selectedNamespace = this.$route.query.namespace || defaultNamespace || "company.team";
|
||||
const selectedNamespace = this.$route.query.namespace || defaultNamespace() || "company.team";
|
||||
flowYaml = `id: ${getRandomID()}
|
||||
namespace: ${selectedNamespace}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
setTimeout(() => {
|
||||
this.flowStore
|
||||
.loadDependencies({namespace: flow.namespace, id: flow.id}, true)
|
||||
.then(({count}) => this.dependenciesCount = count);
|
||||
.then(({count}) => this.dependenciesCount = count > 0 ? (count - 1) : 0);
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
if (flowTriggers) {
|
||||
const triggers = flowTriggers.map(flowTrigger => {
|
||||
let pollingTrigger = this.triggers.find(trigger => trigger.triggerId === flowTrigger.id)
|
||||
return {...flowTrigger, ...(pollingTrigger || {})}
|
||||
return {...flowTrigger, ...pollingTrigger}
|
||||
})
|
||||
|
||||
return !this.query ? triggers : triggers.filter(trigger => trigger.id.includes(this.query))
|
||||
|
||||
@@ -295,7 +295,6 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import {mapState} from "vuex";
|
||||
import {mapStores} from "pinia";
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import _merge from "lodash/merge";
|
||||
@@ -314,7 +313,7 @@
|
||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||
import Kicon from "../Kicon.vue";
|
||||
import Labels from "../layout/Labels.vue";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import YAML_CHART from "../dashboard/assets/executions_timeseries_chart.yaml?raw";
|
||||
import {useAuthStore} from "override/stores/auth.ts";
|
||||
@@ -431,7 +430,6 @@
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState("auth", ["user"]),
|
||||
...mapStores(useExecutionsStore, useFlowStore, useAuthStore),
|
||||
user() {
|
||||
return this.authStore.user;
|
||||
@@ -486,14 +484,11 @@
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, _, next) {
|
||||
const defaultNamespace = localStorage.getItem(
|
||||
storageKeys.DEFAULT_NAMESPACE,
|
||||
);
|
||||
const query = {...to.query};
|
||||
let queryHasChanged = false;
|
||||
const queryKeys = Object.keys(query);
|
||||
if (defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace;
|
||||
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
@@ -796,4 +791,8 @@
|
||||
.flows-table .el-table__cell {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.flows-table) .el-scrollbar__thumb {
|
||||
background-color: var(--ks-border-active) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -252,18 +252,17 @@
|
||||
const settingsEditorFontSize = localStorage.getItem("editorFontSize")
|
||||
|
||||
return {
|
||||
...{
|
||||
tabSize: 2,
|
||||
fontFamily: localStorage.getItem("editorFontFamily")
|
||||
? localStorage.getItem("editorFontFamily")
|
||||
: "'Source Code Pro', monospace",
|
||||
fontSize: settingsEditorFontSize
|
||||
? parseInt(settingsEditorFontSize)
|
||||
: 12,
|
||||
showFoldingControls: "always",
|
||||
scrollBeyondLastLine: false,
|
||||
roundedSelection: false,
|
||||
},
|
||||
|
||||
tabSize: 2,
|
||||
fontFamily: localStorage.getItem("editorFontFamily")
|
||||
? localStorage.getItem("editorFontFamily")
|
||||
: "'Source Code Pro', monospace",
|
||||
fontSize: settingsEditorFontSize
|
||||
? parseInt(settingsEditorFontSize)
|
||||
: 12,
|
||||
showFoldingControls: "always",
|
||||
scrollBeyondLastLine: false,
|
||||
roundedSelection: false,
|
||||
...options,
|
||||
} as monaco.editor.IStandaloneEditorConstructionOptions & {
|
||||
renderSideBySide?:boolean
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<template #absolute>
|
||||
<AITriggerButton
|
||||
:show="isCurrentTabFlow"
|
||||
:opened="aiAgentOpened"
|
||||
@click="draftSource = undefined; aiAgentOpened = true"
|
||||
:opened="aiCopilotOpened"
|
||||
@click="draftSource = undefined; aiCopilotOpened = true"
|
||||
/>
|
||||
<ContentSave v-if="!isCurrentTabFlow" @click="saveFileContent" />
|
||||
</template>
|
||||
@@ -35,13 +35,13 @@
|
||||
</template>
|
||||
</Editor>
|
||||
<Transition name="el-zoom-in-center">
|
||||
<AiAgent
|
||||
v-if="aiAgentOpened"
|
||||
<AiCopilot
|
||||
v-if="aiCopilotOpened"
|
||||
class="position-absolute prompt"
|
||||
@close="aiAgentOpened = false"
|
||||
@close="aiCopilotOpened = false"
|
||||
:flow="editorContent"
|
||||
:conversationId="conversationId"
|
||||
@generated-yaml="(yaml: string) => {draftSource = yaml; aiAgentOpened = false}"
|
||||
@generated-yaml="(yaml: string) => {draftSource = yaml; aiCopilotOpened = false}"
|
||||
/>
|
||||
</Transition>
|
||||
<AcceptDecline
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
import Editor from "./Editor.vue";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import AiAgent from "../ai/AiAgent.vue";
|
||||
import AiCopilot from "../ai/AiCopilot.vue";
|
||||
import AITriggerButton from "../ai/AITriggerButton.vue";
|
||||
import AcceptDecline from "./AcceptDecline.vue";
|
||||
import PlaygroundRunTaskButton from "./PlaygroundRunTaskButton.vue";
|
||||
@@ -88,10 +88,10 @@
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
draftSource.value = undefined;
|
||||
aiAgentOpened.value = !aiAgentOpened.value;
|
||||
aiCopilotOpened.value = !aiCopilotOpened.value;
|
||||
}
|
||||
};
|
||||
const aiAgentOpened = ref(false);
|
||||
const aiCopilotOpened = ref(false);
|
||||
const draftSource = ref<string | undefined>(undefined);
|
||||
|
||||
provide(EDITOR_CURSOR_INJECTION_KEY, cursor);
|
||||
@@ -289,7 +289,7 @@
|
||||
|
||||
function declineDraft() {
|
||||
draftSource.value = undefined;
|
||||
aiAgentOpened.value = true;
|
||||
aiCopilotOpened.value = true;
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
VNode,
|
||||
watch
|
||||
} from "vue";
|
||||
import {useStore} from "vuex";
|
||||
|
||||
import "monaco-editor/esm/vs/editor/editor.all.js";
|
||||
import "monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens.js";
|
||||
@@ -70,7 +69,6 @@
|
||||
import {useFlowStore} from "../../stores/flow.ts";
|
||||
import EditorType = editor.EditorType;
|
||||
|
||||
const store = useStore();
|
||||
const currentInstance = getCurrentInstance()!;
|
||||
const {t} = useI18n();
|
||||
|
||||
@@ -101,7 +99,7 @@
|
||||
});
|
||||
|
||||
import {useRoute} from "vue-router";
|
||||
import {useEditorStore} from "../../stores/editor.ts";
|
||||
import {useEditorStore} from "../../stores/editor";
|
||||
const route = useRoute();
|
||||
|
||||
const highlightLine = () => {
|
||||
@@ -513,7 +511,6 @@
|
||||
|
||||
if (props.language !== undefined) {
|
||||
await configureLanguage(
|
||||
store,
|
||||
flowStore,
|
||||
pluginsStore,
|
||||
t,
|
||||
@@ -727,7 +724,10 @@
|
||||
});
|
||||
|
||||
if (editorRef.value) {
|
||||
localEditor.value = monaco.editor.create(editorRef.value, options);
|
||||
localEditor.value = monaco.editor.create(editorRef.value, {
|
||||
...options,
|
||||
fixedOverflowWidgets: true // Helps suggestion widget render above other elements
|
||||
});
|
||||
|
||||
if (props.suggestionsOnFocus) {
|
||||
localEditor.value.onMouseDown(() => {
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {mapStores} from "pinia";
|
||||
import {groupBy} from "lodash";
|
||||
import _groupBy from "lodash/groupBy";
|
||||
import {useNamespacesStore} from "override/stores/namespaces";
|
||||
import useNamespaces from "../../composables/useNamespaces";
|
||||
import {NamespaceIterator} from "../../composables/useNamespaces";
|
||||
@@ -357,7 +357,7 @@
|
||||
let kvFetch;
|
||||
if (this.namespace === undefined) {
|
||||
if (this.namespaceIterator === undefined) {
|
||||
this.namespaceIterator = useNamespaces(this.$store, 20);
|
||||
this.namespaceIterator = useNamespaces(20);
|
||||
}
|
||||
|
||||
const namespaces = (await ((this.namespaceIterator as NamespaceIterator).next())).map(n => n.id);
|
||||
@@ -425,7 +425,7 @@
|
||||
});
|
||||
},
|
||||
removeKvs() {
|
||||
const groupedByNamespace = groupBy(this.selection, "namespace");
|
||||
const groupedByNamespace = _groupBy(this.selection, "namespace");
|
||||
const withDeletePermissionGroupedKvs = Object.fromEntries(Object.entries(groupedByNamespace).filter(([namespace]) => this.authStore.user.isAllowed(permission.KVSTORE, action.DELETE, namespace)));
|
||||
const withDeletePermissionNamespaces = Object.keys(withDeletePermissionGroupedKvs);
|
||||
const withoutDeletePermissionNamespaces = Object.keys(groupedByNamespace).filter(n => !withDeletePermissionNamespaces.includes(n));
|
||||
|
||||
@@ -122,9 +122,9 @@
|
||||
return this.stillHaveDataToFetch || this.tableView === undefined ? "100%" : `min(${this.tableView.scrollHeight}px, 100%)`;
|
||||
},
|
||||
async infiniteScrollLoadWithDisableHandling() {
|
||||
let load = await this.infiniteScrollLoad();
|
||||
let load = await this.infiniteScrollLoad?.();
|
||||
while (load !== undefined && load.length === 0) {
|
||||
load = await this.infiniteScrollLoad();
|
||||
load = await this.infiniteScrollLoad?.();
|
||||
}
|
||||
|
||||
this.infiniteScrollDisabled = load === undefined;
|
||||
|
||||
@@ -238,6 +238,7 @@
|
||||
background-color: transparent !important;
|
||||
padding-bottom: 15px;
|
||||
width: 30px !important;
|
||||
z-index: 1;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
@@ -268,7 +269,7 @@
|
||||
box-shadow: none;
|
||||
|
||||
&_active, body &_active:hover {
|
||||
background-color: var(--ks-button-background-primary);
|
||||
background-color: var(--ks-button-background-primary) !important;
|
||||
color: var(--ks-button-content-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -338,6 +339,12 @@
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.vsm--link_open.vsm--link_active {
|
||||
.vsm--title, .vsm--icon {
|
||||
color: var(--ks-button-content-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.vsm--arrow_default{
|
||||
width: 8px;
|
||||
&:before{
|
||||
@@ -406,6 +413,21 @@
|
||||
bottom: 0 !important;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.vsm--item {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1.25rem;
|
||||
z-index: 5;
|
||||
background: linear-gradient(to top, var(--ks-background-left-menu), transparent);
|
||||
opacity: 0.18;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,4 +20,5 @@ export const images: Record<string, string> = {
|
||||
plugins,
|
||||
triggers,
|
||||
versionPlugin,
|
||||
panels: triggers // TODO: Replace once https://github.com/kestra-io/kestra/issues/11244 is done
|
||||
};
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
div.line {
|
||||
position: relative;
|
||||
cursor: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
@@ -274,5 +275,16 @@ div.line {
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:deep(.clipboard) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover :deep(.clipboard) {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw";
|
||||
import {useLogsStore} from "../../stores/logs";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
export default {
|
||||
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
||||
@@ -147,15 +148,12 @@
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, _, next) {
|
||||
const defaultNamespace = localStorage.getItem(
|
||||
storageKeys.DEFAULT_NAMESPACE,
|
||||
);
|
||||
const query = {...to.query};
|
||||
let queryHasChanged = false;
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (defaultNamespace && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace;
|
||||
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
@@ -170,12 +168,6 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
LogFilterLanguage() {
|
||||
return LogFilterLanguage
|
||||
},
|
||||
onDateFilterTypeChange(event) {
|
||||
this.canAutoRefresh = event;
|
||||
},
|
||||
showStatChart() {
|
||||
return this.showChart;
|
||||
},
|
||||
|
||||
@@ -409,14 +409,14 @@
|
||||
},
|
||||
methods: {
|
||||
fileUrl(path) {
|
||||
return `${apiUrl(this.$store)}/executions/${this.followedExecution.id}/file?path=${path}`;
|
||||
return `${apiUrl()}/executions/${this.followedExecution.id}/file?path=${path}`;
|
||||
},
|
||||
async fetchAndStoreLogFileSize(path){
|
||||
if (this.logFileSizeByPath[path] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const axiosResponse = await this.$http(`${apiUrl(this.$store)}/executions/${this.followedExecution.id}/file/metas?path=${path}`, {
|
||||
const axiosResponse = await this.$http(`${apiUrl()}/executions/${this.followedExecution.id}/file/metas?path=${path}`, {
|
||||
validateStatus: (status) => status === 200 || status === 404 || status === 422
|
||||
});
|
||||
this.logFileSizeByPath[path] = Utils.humanFileSize(axiosResponse.data.size);
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const main = document.querySelector("main");
|
||||
if(main) main.scrollTop = 0;
|
||||
|
||||
if (namespace.value) {
|
||||
namespacesStore.load(namespace.value);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
import {useNamespacesStore} from "override/stores/namespaces"
|
||||
import DotsSquare from "vue-material-design-icons/DotsSquare.vue"
|
||||
import Lock from "vue-material-design-icons/Lock.vue";
|
||||
import {storageKeys} from "../../../utils/constants";
|
||||
import {defaultNamespace} from "../../../composables/useNamespaces";
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
@@ -79,13 +79,13 @@
|
||||
|
||||
onMounted(() => {
|
||||
if (modelValue.value === undefined || modelValue.value.length === 0) {
|
||||
const defaultNamespace = localStorage.getItem(storageKeys.DEFAULT_NAMESPACE);
|
||||
const defaultNamespaceVal = defaultNamespace();
|
||||
if (Array.isArray(modelValue.value)) {
|
||||
if (defaultNamespace != null) {
|
||||
modelValue.value = [defaultNamespace];
|
||||
if (defaultNamespaceVal != null) {
|
||||
modelValue.value = [defaultNamespaceVal];
|
||||
}
|
||||
} else {
|
||||
modelValue.value = defaultNamespace ?? modelValue.value;
|
||||
modelValue.value = defaultNamespaceVal ?? modelValue.value;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
UPDATE_TASK_FUNCTION_INJECTION_KEY,
|
||||
} from "./injectionKeys";
|
||||
import {useFlowFields, SECTIONS_IDS} from "./utils/useFlowFields";
|
||||
import {debounce} from "lodash";
|
||||
import debounce from "lodash/debounce";
|
||||
import {NoCodeProps} from "../flows/noCodeTypes";
|
||||
import {useEditorStore} from "../../stores/editor";
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<template>
|
||||
<el-collapse v-model="expanded" class="collapse">
|
||||
<el-collapse-item
|
||||
:name="title"
|
||||
:title="`${title}${elements ? ` (${elements.length})` : ''}`"
|
||||
>
|
||||
<template #icon>
|
||||
<Creation
|
||||
:parentPathComplete="parentPathComplete"
|
||||
:refPath="elements?.length ? elements.length - 1 : undefined"
|
||||
:blockSchemaPath
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Element
|
||||
v-for="(element, elementIndex) in filteredElements"
|
||||
:key="elementIndex"
|
||||
:section="section"
|
||||
:parentPathComplete="parentPathComplete"
|
||||
:element
|
||||
:elementIndex="elementIndex"
|
||||
:moved="elementIndex == movedIndex"
|
||||
:blockSchemaPath
|
||||
:typeFieldSchema
|
||||
@remove-element="removeElement(elementIndex)"
|
||||
@move-element="
|
||||
(direction: 'up' | 'down') =>
|
||||
moveElement(
|
||||
elements,
|
||||
element.id,
|
||||
elementIndex,
|
||||
direction,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from "vue";
|
||||
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
|
||||
import {CollapseItem} from "../../utils/types";
|
||||
|
||||
import Creation from "./buttons/Creation.vue";
|
||||
import Element from "./Element.vue";
|
||||
import {
|
||||
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
|
||||
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
|
||||
} from "../../injectionKeys";
|
||||
import {SECTIONS_MAP} from "../../../../utils/constants";
|
||||
import {getValueAtJsonPath} from "../../../../utils/utils";
|
||||
|
||||
const emits = defineEmits(["remove", "reorder"]);
|
||||
|
||||
const flow = inject(FULL_SOURCE_INJECTION_KEY, ref(""));
|
||||
|
||||
const props = defineProps<CollapseItem>();
|
||||
const filteredElements = computed(() => props.elements?.filter(Boolean) ?? []);
|
||||
const expanded = ref<CollapseItem["title"]>(props.title);
|
||||
|
||||
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
||||
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
||||
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, false);
|
||||
|
||||
const parentPathComplete = computed(() => {
|
||||
return `${[
|
||||
[
|
||||
parentPath,
|
||||
creatingTask && refPath !== undefined
|
||||
? `[${refPath + 1}]`
|
||||
: refPath !== undefined
|
||||
? `[${refPath}]`
|
||||
: undefined,
|
||||
].filter(Boolean).join(""),
|
||||
props.section
|
||||
].filter(p => p.length).join(".")}`;
|
||||
});
|
||||
|
||||
const removeElement = (index: number) => {
|
||||
emits(
|
||||
"remove",
|
||||
YAML_UTILS.deleteBlockWithPath({
|
||||
source: flow.value,
|
||||
path: `${parentPathComplete.value}[${index}]`
|
||||
}),
|
||||
index
|
||||
);
|
||||
};
|
||||
|
||||
const movedIndex = ref(-1);
|
||||
|
||||
const moveElement = (
|
||||
items: Record<string, any>[] | undefined,
|
||||
elementID: string,
|
||||
index: number,
|
||||
direction: "up" | "down",
|
||||
) => {
|
||||
const keyName = props.title === "Plugin Defaults" ? "type" : "id";
|
||||
if (!items || !flow) return;
|
||||
if (
|
||||
(direction === "up" && index === 0) ||
|
||||
(direction === "down" && index === items.length - 1)
|
||||
)
|
||||
return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
|
||||
movedIndex.value = newIndex;
|
||||
setTimeout(() => {
|
||||
movedIndex.value = -1;
|
||||
}, 200);
|
||||
|
||||
emits(
|
||||
"reorder",
|
||||
YAML_UTILS.swapBlocks({
|
||||
source:flow.value,
|
||||
section: SECTIONS_MAP[props.title.toLowerCase() as keyof typeof SECTIONS_MAP],
|
||||
key1:elementID,
|
||||
key2:items[newIndex][keyName],
|
||||
keyName,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));
|
||||
|
||||
// resolve parentPathComplete field schema from pluginsStore
|
||||
const typeFieldSchema = computed(() => {
|
||||
const blockSchema = getValueAtJsonPath(fullSchema.value, props.blockSchemaPath)?.properties;
|
||||
return blockSchema?.type ? "type" : blockSchema?.on ? "on" : "type";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/code.scss";
|
||||
</style>
|
||||
@@ -1,23 +1,63 @@
|
||||
<template>
|
||||
<div class="tasks-wrapper">
|
||||
<Collapse
|
||||
:title="root"
|
||||
:elements="items"
|
||||
:section
|
||||
:blockSchemaPath="[blockSchemaPath, 'properties', root, 'items'].join('/')"
|
||||
@remove="removeItem"
|
||||
@reorder="(yaml) => flowStore.flowYaml = yaml"
|
||||
/>
|
||||
<el-collapse v-model="expanded" class="collapse">
|
||||
<el-collapse-item
|
||||
:name="sectionName"
|
||||
:title="`${sectionName}${elements ? ` (${elements.length})` : ''}`"
|
||||
>
|
||||
<template #icon>
|
||||
<Creation
|
||||
:parentPathComplete="parentPathComplete"
|
||||
:refPath="elements?.length ? elements.length - 1 : undefined"
|
||||
:blockSchemaPath
|
||||
/>
|
||||
</template>
|
||||
|
||||
<Element
|
||||
v-for="(element, elementIndex) in filteredElements"
|
||||
:key="elementIndex"
|
||||
:section="sectionName"
|
||||
:parentPathComplete="parentPathComplete"
|
||||
:element
|
||||
:elementIndex="elementIndex"
|
||||
:moved="elementIndex == movedIndex"
|
||||
:blockSchemaPath
|
||||
:typeFieldSchema
|
||||
@remove-element="removeElement(elementIndex)"
|
||||
@move-element="
|
||||
(direction: 'up' | 'down') =>
|
||||
moveElement(
|
||||
elements,
|
||||
element.id,
|
||||
elementIndex,
|
||||
direction,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from "vue";
|
||||
import Collapse from "../collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
import Creation from "./taskList/buttons/Creation.vue";
|
||||
import Element from "./taskList/Element.vue";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
|
||||
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""))
|
||||
import {CollapseItem} from "../../utils/types";
|
||||
|
||||
import {
|
||||
CREATING_TASK_INJECTION_KEY, FULL_SCHEMA_INJECTION_KEY, FULL_SOURCE_INJECTION_KEY,
|
||||
PARENT_PATH_INJECTION_KEY, REF_PATH_INJECTION_KEY,
|
||||
} from "../../injectionKeys";
|
||||
import {SECTIONS_MAP} from "../../../../utils/constants";
|
||||
import {getValueAtJsonPath} from "../../../../utils/utils";
|
||||
|
||||
const blockSchemaPathInjected = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""))
|
||||
const blockSchemaPath = computed(() => [blockSchemaPathInjected.value, "properties", props.root, "items"].join("/"));
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
@@ -39,22 +79,90 @@
|
||||
root: undefined
|
||||
});
|
||||
|
||||
const items = computed(() =>
|
||||
const elements = computed(() =>
|
||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||
);
|
||||
|
||||
function removeItem(yaml: string, index: number){
|
||||
flowStore.flowYaml = yaml;
|
||||
|
||||
let localItems = [...items.value]
|
||||
function removeElement(index: number){
|
||||
if(elements.value.length <= 1){
|
||||
emits("update:modelValue", undefined);
|
||||
return
|
||||
}
|
||||
let localItems = [...elements.value]
|
||||
localItems.splice(index, 1)
|
||||
|
||||
emits("update:modelValue", localItems);
|
||||
};
|
||||
|
||||
const section = computed(() => {
|
||||
const sectionName = computed(() => {
|
||||
return props.root ?? "tasks";
|
||||
});
|
||||
|
||||
|
||||
|
||||
const flow = inject(FULL_SOURCE_INJECTION_KEY, ref(""));
|
||||
|
||||
const filteredElements = computed(() => elements.value?.filter(Boolean) ?? []);
|
||||
const expanded = ref<CollapseItem["title"]>(props.root ?? "tasks");
|
||||
|
||||
const parentPath = inject(PARENT_PATH_INJECTION_KEY, "");
|
||||
const refPath = inject(REF_PATH_INJECTION_KEY, undefined);
|
||||
const creatingTask = inject(CREATING_TASK_INJECTION_KEY, false);
|
||||
|
||||
const parentPathComplete = computed(() => {
|
||||
return `${[
|
||||
[
|
||||
parentPath,
|
||||
creatingTask && refPath !== undefined
|
||||
? `[${refPath + 1}]`
|
||||
: refPath !== undefined
|
||||
? `[${refPath}]`
|
||||
: undefined,
|
||||
].filter(Boolean).join(""),
|
||||
sectionName.value
|
||||
].filter(p => p.length).join(".")}`;
|
||||
});
|
||||
|
||||
const movedIndex = ref(-1);
|
||||
|
||||
const moveElement = (
|
||||
items: Record<string, any>[] | undefined,
|
||||
elementID: string,
|
||||
index: number,
|
||||
direction: "up" | "down",
|
||||
) => {
|
||||
const keyName = sectionName.value === "Plugin Defaults" ? "type" : "id";
|
||||
if (!items || !flow) return;
|
||||
if (
|
||||
(direction === "up" && index === 0) ||
|
||||
(direction === "down" && index === items.length - 1)
|
||||
)
|
||||
return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
|
||||
movedIndex.value = newIndex;
|
||||
setTimeout(() => {
|
||||
movedIndex.value = -1;
|
||||
}, 200);
|
||||
|
||||
flowStore.flowYaml =
|
||||
YAML_UTILS.swapBlocks({
|
||||
source:flow.value,
|
||||
section: SECTIONS_MAP[sectionName.value.toLowerCase() as keyof typeof SECTIONS_MAP],
|
||||
key1:elementID,
|
||||
key2:items[newIndex][keyName],
|
||||
keyName,
|
||||
})
|
||||
};
|
||||
|
||||
const fullSchema = inject(FULL_SCHEMA_INJECTION_KEY, ref<Record<string, any>>({}));
|
||||
|
||||
// resolve parentPathComplete field schema from pluginsStore
|
||||
const typeFieldSchema = computed(() => {
|
||||
const blockSchema = getValueAtJsonPath(fullSchema.value, blockSchemaPath.value)?.properties;
|
||||
return blockSchema?.type ? "type" : blockSchema?.on ? "on" : "type";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
CREATING_TASK_INJECTION_KEY,
|
||||
BLOCK_SCHEMA_PATH_INJECTION_KEY
|
||||
} from "../../injectionKeys";
|
||||
import Element from "../collapse/Element.vue";
|
||||
import Element from "./taskList/Element.vue";
|
||||
|
||||
const model = defineModel({
|
||||
type: Object,
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<div class="tasks-wrapper">
|
||||
<Collapse
|
||||
title="tasks"
|
||||
:elements="items"
|
||||
:section
|
||||
:blockSchemaPath="[blockSchemaPath, 'properties', root, 'items'].join('/')"
|
||||
@remove="(yaml) => flowStore.flowYaml = yaml"
|
||||
@reorder="(yaml) => flowStore.flowYaml = yaml"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from "vue";
|
||||
import Collapse from "../collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
|
||||
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref())
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
const flowStore = useFlowStore();
|
||||
|
||||
interface Task {
|
||||
id:string,
|
||||
type:string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: Task[],
|
||||
root?: string;
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
root: undefined
|
||||
});
|
||||
|
||||
const items = computed(() =>
|
||||
!Array.isArray(props.modelValue) ? [props.modelValue] : props.modelValue,
|
||||
);
|
||||
|
||||
const section = computed(() => {
|
||||
return props.root ?? "tasks";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.tasks-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -29,14 +29,14 @@
|
||||
import {computed, inject} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import PlayIcon from "vue-material-design-icons/Play.vue";
|
||||
import {usePluginsStore} from "../../../../stores/plugins";
|
||||
import {usePlaygroundStore} from "../../../../stores/playground";
|
||||
import {usePluginsStore} from "../../../../../stores/plugins";
|
||||
import {usePlaygroundStore} from "../../../../../stores/playground";
|
||||
|
||||
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../../utils/icons";
|
||||
import {
|
||||
EDIT_TASK_FUNCTION_INJECTION_KEY,
|
||||
} from "../../injectionKeys";
|
||||
} from "../../../injectionKeys";
|
||||
|
||||
import TaskIcon from "@kestra-io/ui-libs/src/components/misc/TaskIcon.vue";
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/code.scss";
|
||||
@import "../../../styles/code.scss";
|
||||
@import "@kestra-io/ui-libs/src/scss/_color-palette";
|
||||
|
||||
.element {
|
||||
@@ -8,8 +8,8 @@
|
||||
import {inject} from "vue";
|
||||
import {
|
||||
CREATE_TASK_FUNCTION_INJECTION_KEY,
|
||||
} from "../../../injectionKeys";
|
||||
import {Plus} from "../../../utils/icons";
|
||||
} from "../../../../injectionKeys";
|
||||
import {Plus} from "../../../../utils/icons";
|
||||
|
||||
|
||||
import {useI18n} from "vue-i18n";
|
||||
@@ -319,7 +319,7 @@
|
||||
},
|
||||
async fetchSecrets() {
|
||||
if (this.secretsIterator === undefined) {
|
||||
this.secretsIterator = this.namespace === undefined ? useAllSecrets(this.$store, this.authStore.user, 20) : useNamespaceSecrets(this.$store, this.namespace, 20, {
|
||||
this.secretsIterator = this.namespace === undefined ? useAllSecrets(this.authStore.user, 20) : useNamespaceSecrets(this.namespace, 20, {
|
||||
sort: this.$route.query.sort || "key:asc",
|
||||
...(this.searchQuery === undefined ? {} : {filters: {
|
||||
q: {
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
import Column from "./components/block/Column.vue"
|
||||
import {useAuthStore} from "override/stores/auth"
|
||||
import {useFlowStore} from "../../stores/flow"
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
export const DATE_FORMAT_STORAGE_KEY = "dateFormat";
|
||||
export const TIMEZONE_STORAGE_KEY = "timezone";
|
||||
@@ -342,7 +343,7 @@
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.pendingSettings.defaultNamespace = localStorage.getItem("defaultNamespace") || "company.team";
|
||||
this.pendingSettings.defaultNamespace = defaultNamespace();
|
||||
this.pendingSettings.editorType = localStorage.getItem(storageKeys.EDITOR_VIEW_TYPE) || "YAML";
|
||||
this.pendingSettings.defaultLogLevel = localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
this.pendingSettings.lang = Utils.getLang();
|
||||
@@ -547,7 +548,7 @@
|
||||
}
|
||||
break
|
||||
case "theme":
|
||||
Utils.switchTheme(this.$store, this.pendingSettings[key]);
|
||||
Utils.switchTheme(this.miscStore, this.pendingSettings[key]);
|
||||
localStorage.setItem(key, Utils.getTheme())
|
||||
break
|
||||
case "lang":
|
||||
@@ -591,7 +592,7 @@
|
||||
},
|
||||
updateThemeBasedOnSystem() {
|
||||
if (this.theme === "syncWithSystem") {
|
||||
Utils.switchTheme(this.$store, "syncWithSystem");
|
||||
Utils.switchTheme(this.miscStore, "syncWithSystem");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -173,9 +173,6 @@
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDateFilterTypeChange(event) {
|
||||
this.canAutoRefresh = event;
|
||||
},
|
||||
isRunning(item){
|
||||
return State.isRunning(item.state.current);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import {IDisposable} from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import {Store} from "vuex";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {usePluginsStore} from "../../../stores/plugins.ts";
|
||||
|
||||
@@ -20,13 +19,13 @@ export default abstract class AbstractLanguageConfigurator {
|
||||
monaco.languages.register({id: this.language});
|
||||
}
|
||||
|
||||
abstract configureAutoCompletion(t: ReturnType<typeof useI18n>["t"], store: Store<Record<string, any>>, editorInstance: monaco.editor.ICodeEditor | undefined): IDisposable[];
|
||||
abstract configureAutoCompletion(t: ReturnType<typeof useI18n>["t"], editorInstance: monaco.editor.ICodeEditor | undefined): IDisposable[];
|
||||
|
||||
async configure(store: Store<Record<string, any>>, pluginsStore: ReturnType<typeof usePluginsStore>, t: ReturnType<typeof useI18n>["t"], editorInstance: monaco.editor.ICodeEditor | undefined): Promise<IDisposable[]> {
|
||||
async configure(pluginsStore: ReturnType<typeof usePluginsStore>, t: ReturnType<typeof useI18n>["t"], editorInstance: monaco.editor.ICodeEditor | undefined): Promise<IDisposable[]> {
|
||||
if (!AbstractLanguageConfigurator.configuredLanguages.includes(this.language)) {
|
||||
AbstractLanguageConfigurator.configuredLanguages.push(this.language);
|
||||
await this.configureLanguage(pluginsStore);
|
||||
return this.configureAutoCompletion(t, store, editorInstance);
|
||||
return this.configureAutoCompletion(t, editorInstance);
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useValues} from "../../../../components/filter/composables/useValues.ts";
|
||||
import {Value} from "../../../../components/filter/utils/types.ts";
|
||||
import {Store} from "vuex";
|
||||
|
||||
export enum Comparators {
|
||||
EQUALS = "=",
|
||||
@@ -56,7 +55,7 @@ export const PICK_DATE_VALUE = "PICK_DATE";
|
||||
|
||||
export type ValueCompletions = Value[] | typeof PICK_DATE_VALUE;
|
||||
|
||||
export type Fetcher = (store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions>;
|
||||
export type Fetcher = (hardcodedValues: ReturnType<typeof useValues>["VALUES"]) => Promise<ValueCompletions>;
|
||||
|
||||
export class FilterKeyCompletions {
|
||||
private readonly _comparators: Comparators[];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Comparators, Completion, FilterKeyCompletions, keyOfComparator, ValueCompletions} from "./filterCompletion";
|
||||
import {useValues} from "../../../../components/filter/composables/useValues";
|
||||
import {Store} from "vuex";
|
||||
|
||||
type FilterKeyCompletionEntries = [
|
||||
({ key: string, regex: RegExp }),
|
||||
@@ -23,15 +22,13 @@ export abstract class FilterLanguage {
|
||||
|
||||
protected constructor(domain: string | undefined, filterKeyCompletions: Record<string, FilterKeyCompletions>, textFilterSupported: boolean = true) {
|
||||
this._domain = domain;
|
||||
this._filterKeyCompletions = [
|
||||
...(Object.entries(filterKeyCompletions).map(([key, filterKeyCompletion]) => [
|
||||
this._filterKeyCompletions = (Object.entries(filterKeyCompletions).map(([key, filterKeyCompletion]) => [
|
||||
{
|
||||
key: key,
|
||||
regex: new RegExp("^" + key.replaceAll(".", "\\.").replaceAll(/\$?\{([^}]*)}/g, ".*") + "$")
|
||||
},
|
||||
filterKeyCompletion
|
||||
]) as FilterKeyCompletionEntries)
|
||||
];
|
||||
]) as FilterKeyCompletionEntries);
|
||||
this._textFilterSupported = textFilterSupported;
|
||||
|
||||
if (textFilterSupported) {
|
||||
@@ -86,13 +83,13 @@ export abstract class FilterLanguage {
|
||||
return completion.comparators.map(comparator => new Completion(keyOfComparator(comparator), comparator));
|
||||
}
|
||||
|
||||
async valueCompletion(store: Store<Record<string, any>>, hardcodedValues: ReturnType<typeof useValues>["VALUES"], key: string): Promise<ValueCompletions> {
|
||||
async valueCompletion(hardcodedValues: ReturnType<typeof useValues>["VALUES"], key: string): Promise<ValueCompletions> {
|
||||
const completion = this.completionForKey(key);
|
||||
if (completion === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return completion.valuesFetcher(store, hardcodedValues);
|
||||
return completion.valuesFetcher(hardcodedValues);
|
||||
}
|
||||
|
||||
multipleValuesAllowed(key: string): boolean {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import {editor, IPosition, IRange} from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import AbstractLanguageConfigurator from "../abstractLanguageConfigurator";
|
||||
import {Store} from "vuex";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {FilterLanguage} from "./filterLanguage";
|
||||
import {useValues} from "../../../../components/filter/composables/useValues";
|
||||
@@ -39,7 +38,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
return legacyFilterRegex.test(this.language);
|
||||
}
|
||||
|
||||
async configure(store: Store<Record<string, any>>, pluginsStore: ReturnType<typeof usePluginsStore>, t: ReturnType<typeof useI18n>["t"], editorInstance: editor.ICodeEditor | undefined): Promise<monaco.IDisposable[]> {
|
||||
async configure(pluginsStore: ReturnType<typeof usePluginsStore>, t: ReturnType<typeof useI18n>["t"], editorInstance: editor.ICodeEditor | undefined): Promise<monaco.IDisposable[]> {
|
||||
filterLanguages = await loadFilterLanguages();
|
||||
|
||||
this._filterLanguage = filterLanguages.find(filterLanguage => filterLanguage.domain === this._domain);
|
||||
@@ -54,7 +53,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
?.join("|") + ")"
|
||||
));
|
||||
|
||||
return super.configure(store, pluginsStore, t, editorInstance);
|
||||
return super.configure(pluginsStore, t, editorInstance);
|
||||
}
|
||||
|
||||
async configureLanguage(): Promise<void> {
|
||||
@@ -147,7 +146,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
}
|
||||
}
|
||||
|
||||
configureAutoCompletion(t: ReturnType<typeof useI18n>["t"], store: Store<Record<string, any>>, __: editor.ICodeEditor | undefined) {
|
||||
configureAutoCompletion(t: ReturnType<typeof useI18n>["t"], __: editor.ICodeEditor | undefined) {
|
||||
const filterLanguage = this._filterLanguage;
|
||||
if (filterLanguage === undefined) {
|
||||
return [];
|
||||
@@ -318,7 +317,6 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
|
||||
if (key !== undefined) {
|
||||
const valueCompletions = await filterLanguage.valueCompletion(
|
||||
store,
|
||||
hardcodedValues,
|
||||
key
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user