mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 14:00:23 -05:00
Compare commits
23 Commits
debug-flak
...
fix/left-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5608e08d8 | ||
|
|
9fb63284f0 | ||
|
|
bbd28ad2a8 | ||
|
|
32b6e8c6d7 | ||
|
|
93adccb716 | ||
|
|
b6b854598b | ||
|
|
cf42fe751e | ||
|
|
b144fae047 | ||
|
|
fc59fd7505 | ||
|
|
65eeea8256 | ||
|
|
4769fa2ac5 | ||
|
|
9a4b569d85 | ||
|
|
1abef5429c | ||
|
|
bdbd9d45f8 | ||
|
|
7d1f064fe9 | ||
|
|
a125c8d314 | ||
|
|
a9d27d4757 | ||
|
|
d97f3a101c | ||
|
|
a65310bcab | ||
|
|
58e5efe767 | ||
|
|
c3c46ae336 | ||
|
|
f8bb59f76e | ||
|
|
0c4425b030 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -32,12 +32,13 @@ ui/node_modules
|
||||
ui/.env.local
|
||||
ui/.env.*.local
|
||||
webserver/src/main/resources/ui
|
||||
yarn.lock
|
||||
webserver/src/main/resources/views
|
||||
ui/coverage
|
||||
ui/stats.html
|
||||
ui/.frontend-gradle-plugin
|
||||
ui/utils/CHANGELOG.md
|
||||
ui/test-report.junit.xml
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
### Docker
|
||||
/.env
|
||||
@@ -57,6 +58,4 @@ core/src/main/resources/gradle.properties
|
||||
# Allure Reports
|
||||
**/allure-results/*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
/jmh-benchmarks/src/main/resources/gradle.properties
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.filters.AbstractFilter;
|
||||
import io.kestra.core.repositories.QueryBuilderInterface;
|
||||
import io.kestra.plugin.core.dashboard.data.IData;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
@@ -33,9 +34,11 @@ public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F
|
||||
@Pattern(regexp = JAVA_IDENTIFIER_REGEX)
|
||||
private String type;
|
||||
|
||||
@Valid
|
||||
private Map<String, C> columns;
|
||||
|
||||
@Setter
|
||||
@Valid
|
||||
private List<AbstractFilter<F>> where;
|
||||
|
||||
private List<OrderBy> orderBy;
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.kestra.core.models.annotations.Plugin;
|
||||
import io.kestra.core.models.dashboards.ChartOption;
|
||||
import io.kestra.core.models.dashboards.DataFilter;
|
||||
import io.kestra.core.validations.DataChartValidation;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@@ -20,6 +21,7 @@ import lombok.experimental.SuperBuilder;
|
||||
@DataChartValidation
|
||||
public abstract class DataChart<P extends ChartOption, D extends DataFilter<?, ?>> extends Chart<P> implements io.kestra.core.models.Plugin {
|
||||
@NotNull
|
||||
@Valid
|
||||
private D data;
|
||||
|
||||
public Integer minNumberOfAggregations() {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package io.kestra.core.models.dashboards.filters;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.micronaut.core.annotation.Introspected;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -32,6 +35,9 @@ import lombok.experimental.SuperBuilder;
|
||||
@SuperBuilder
|
||||
@Introspected
|
||||
public abstract class AbstractFilter<F extends Enum<F>> {
|
||||
@NotNull
|
||||
@JsonProperty(value = "field", required = true)
|
||||
@Valid
|
||||
private F field;
|
||||
private String labelKey;
|
||||
|
||||
|
||||
@@ -82,8 +82,7 @@ public abstract class FilesService {
|
||||
}
|
||||
|
||||
private static String resolveUniqueNameForFile(final Path path) {
|
||||
String filename = path.getFileName().toString();
|
||||
String encodedFilename = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8);
|
||||
return IdUtils.from(path.toString()) + "-" + encodedFilename;
|
||||
String filename = path.getFileName().toString().replace(' ', '+');
|
||||
return IdUtils.from(path.toString()) + "-" + filename;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +151,7 @@ abstract class AbstractFileFunction implements Function {
|
||||
// if there is a trigger of type execution, we also allow accessing a file from the parent execution
|
||||
Map<String, String> trigger = (Map<String, String>) context.getVariable(TRIGGER);
|
||||
|
||||
if (!isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path)) {
|
||||
throw new IllegalArgumentException("Unable to read the file '" + path + "' as it didn't belong to the parent execution");
|
||||
}
|
||||
return true;
|
||||
return isFileUriValid(trigger.get(NAMESPACE), trigger.get("flowId"), trigger.get("executionId"), path);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import io.kestra.core.annotations.Retryable;
|
||||
import io.kestra.core.models.Plugin;
|
||||
import io.kestra.core.models.executions.Execution;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
@@ -361,7 +362,7 @@ public interface StorageInterface extends AutoCloseable, Plugin {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Ensures the object name length does not exceed the allowed maximum.
|
||||
* If it does, the object name is truncated and a short random prefix is added
|
||||
* to avoid potential name collisions.
|
||||
@@ -378,10 +379,9 @@ public interface StorageInterface extends AutoCloseable, Plugin {
|
||||
|
||||
String path = uri.getPath();
|
||||
String objectName = path.contains("/") ? path.substring(path.lastIndexOf("/") + 1) : path;
|
||||
|
||||
if (objectName.length() > maxObjectNameLength) {
|
||||
objectName = objectName.substring(objectName.length() - maxObjectNameLength + 6);
|
||||
String prefix = org.apache.commons.lang3.RandomStringUtils.secure()
|
||||
String prefix = RandomStringUtils.secure()
|
||||
.nextAlphanumeric(5)
|
||||
.toLowerCase();
|
||||
|
||||
|
||||
@@ -106,28 +106,28 @@ class FilesServiceTest {
|
||||
var runContext = runContextFactory.of();
|
||||
|
||||
Path fileWithSpace = tempDir.resolve("with space.txt");
|
||||
Path fileWithUnicode = tempDir.resolve("สวัสดี.txt");
|
||||
Path fileWithUnicode = tempDir.resolve("สวัสดี&.txt");
|
||||
|
||||
Files.writeString(fileWithSpace, "content");
|
||||
Files.writeString(fileWithUnicode, "content");
|
||||
|
||||
Path targetFileWithSpace = runContext.workingDir().path().resolve("with space.txt");
|
||||
Path targetFileWithUnicode = runContext.workingDir().path().resolve("สวัสดี.txt");
|
||||
Path targetFileWithUnicode = runContext.workingDir().path().resolve("สวัสดี&.txt");
|
||||
|
||||
Files.copy(fileWithSpace, targetFileWithSpace);
|
||||
Files.copy(fileWithUnicode, targetFileWithUnicode);
|
||||
|
||||
Map<String, URI> outputFiles = FilesService.outputFiles(
|
||||
runContext,
|
||||
List.of("with space.txt", "สวัสดี.txt")
|
||||
List.of("with space.txt", "สวัสดี&.txt")
|
||||
);
|
||||
|
||||
assertThat(outputFiles).hasSize(2);
|
||||
assertThat(outputFiles).containsKey("with space.txt");
|
||||
assertThat(outputFiles).containsKey("สวัสดี.txt");
|
||||
assertThat(outputFiles).containsKey("สวัสดี&.txt");
|
||||
|
||||
assertThat(runContext.storage().getFile(outputFiles.get("with space.txt"))).isNotNull();
|
||||
assertThat(runContext.storage().getFile(outputFiles.get("สวัสดี.txt"))).isNotNull();
|
||||
assertThat(runContext.storage().getFile(outputFiles.get("สวัสดี&.txt"))).isNotNull();
|
||||
}
|
||||
|
||||
private URI createFile() throws IOException {
|
||||
|
||||
@@ -112,33 +112,6 @@ public class FileSizeFunctionTest {
|
||||
assertThat(size).isEqualTo(FILE_SIZE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowIllegalArgumentException_givenTrigger_andParentExecution_andMissingNamespace() throws IOException {
|
||||
String executionId = IdUtils.create();
|
||||
URI internalStorageURI = getInternalStorageURI(executionId);
|
||||
URI internalStorageFile = getInternalStorageFile(internalStorageURI);
|
||||
|
||||
Map<String, Object> variables = Map.of(
|
||||
"flow", Map.of(
|
||||
"id", "subflow",
|
||||
"namespace", NAMESPACE,
|
||||
"tenantId", MAIN_TENANT),
|
||||
"execution", Map.of("id", IdUtils.create()),
|
||||
"trigger", Map.of(
|
||||
"flowId", FLOW,
|
||||
"executionId", executionId,
|
||||
"tenantId", MAIN_TENANT
|
||||
)
|
||||
);
|
||||
|
||||
Exception ex = assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> variableRenderer.render("{{ fileSize('" + internalStorageFile + "') }}", variables)
|
||||
);
|
||||
|
||||
assertTrue(ex.getMessage().startsWith("Unable to read the file"), "Exception message doesn't match expected one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsCorrectSize_givenUri_andCurrentExecution() throws IOException, IllegalVariableEvaluationException {
|
||||
String executionId = IdUtils.create();
|
||||
|
||||
@@ -259,6 +259,27 @@ class ReadFileFunctionTest {
|
||||
assertThat(variableRenderer.render("{{ read(nsfile) }}", variables)).isEqualTo("Hello World");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadChildFileEvenIfTrigger() throws IOException, IllegalVariableEvaluationException {
|
||||
String namespace = "my.namespace";
|
||||
String flowId = "flow";
|
||||
String executionId = IdUtils.create();
|
||||
URI internalStorageURI = URI.create("/" + namespace.replace(".", "/") + "/" + flowId + "/executions/" + executionId + "/tasks/task/" + IdUtils.create() + "/123456.ion");
|
||||
URI internalStorageFile = storageInterface.put(MAIN_TENANT, namespace, internalStorageURI, new ByteArrayInputStream("Hello from a task output".getBytes()));
|
||||
|
||||
Map<String, Object> variables = Map.of(
|
||||
"flow", Map.of(
|
||||
"id", "flow",
|
||||
"namespace", "notme",
|
||||
"tenantId", MAIN_TENANT),
|
||||
"execution", Map.of("id", "notme"),
|
||||
"trigger", Map.of("namespace", "notme", "flowId", "parent", "executionId", "parent")
|
||||
);
|
||||
|
||||
String render = variableRenderer.render("{{ read('" + internalStorageFile + "') }}", variables);
|
||||
assertThat(render).isEqualTo("Hello from a task output");
|
||||
}
|
||||
|
||||
private URI createFile() throws IOException {
|
||||
File tempFile = File.createTempFile("file", ".txt");
|
||||
Files.write(tempFile.toPath(), "Hello World".getBytes());
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRepository {
|
||||
protected io.kestra.jdbc.AbstractJdbcRepository<ExecutionQueued> jdbcRepository;
|
||||
@@ -70,18 +69,12 @@ public abstract class AbstractJdbcExecutionQueuedStorage extends AbstractJdbcRep
|
||||
this.jdbcRepository
|
||||
.getDslContextWrapper()
|
||||
.transaction(configuration -> {
|
||||
var select = DSL
|
||||
.using(configuration)
|
||||
.select(AbstractJdbcRepository.field("value"))
|
||||
.from(this.jdbcRepository.getTable())
|
||||
.where(buildTenantCondition(execution.getTenantId()))
|
||||
.and(field("key").eq(IdUtils.fromParts(execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), execution.getId())))
|
||||
.forUpdate();
|
||||
|
||||
Optional<ExecutionQueued> maybeExecution = this.jdbcRepository.fetchOne(select);
|
||||
if (maybeExecution.isPresent()) {
|
||||
this.jdbcRepository.delete(maybeExecution.get());
|
||||
}
|
||||
DSL
|
||||
.using(configuration)
|
||||
.deleteFrom(this.jdbcRepository.getTable())
|
||||
.where(buildTenantCondition(execution.getTenantId()))
|
||||
.and(field("key").eq(IdUtils.fromParts(execution.getTenantId(), execution.getNamespace(), execution.getFlowId(), execution.getId())))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import static io.kestra.core.utils.WindowsUtils.windowsToUnixPath;
|
||||
@NoArgsConstructor
|
||||
public class LocalStorage implements StorageInterface {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalStorage.class);
|
||||
private static final int MAX_OBJECT_NAME_LENGTH = 255;
|
||||
|
||||
@PluginProperty
|
||||
@NotNull
|
||||
@@ -170,14 +171,16 @@ public class LocalStorage implements StorageInterface {
|
||||
|
||||
@Override
|
||||
public URI put(String tenantId, @Nullable String namespace, URI uri, StorageObject storageObject) throws IOException {
|
||||
File file = getLocalPath(tenantId, uri).toFile();
|
||||
return putFile(uri, storageObject, file);
|
||||
URI limited = limit(uri, MAX_OBJECT_NAME_LENGTH);
|
||||
File file = getLocalPath(tenantId, limited).toFile();
|
||||
return putFile(limited, storageObject, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI putInstanceResource(@Nullable String namespace, URI uri, StorageObject storageObject) throws IOException {
|
||||
File file = getInstancePath(uri).toFile();
|
||||
return putFile(uri, storageObject, file);
|
||||
URI limited = limit(uri, MAX_OBJECT_NAME_LENGTH);
|
||||
File file = getInstancePath(limited).toFile();
|
||||
return putFile(limited, storageObject, file);
|
||||
}
|
||||
|
||||
private static URI putFile(URI uri, StorageObject storageObject, File file) throws IOException {
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
package io.kestra.storage.local;
|
||||
|
||||
import io.kestra.core.storage.StorageTestSuite;
|
||||
import io.kestra.core.utils.IdUtils;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class LocalStorageTest extends StorageTestSuite {
|
||||
// Launch test from StorageTestSuite
|
||||
|
||||
@Test
|
||||
void putLongObjectName() throws URISyntaxException, IOException {
|
||||
String longObjectName = "/" + RandomStringUtils.insecure().nextAlphanumeric(260).toLowerCase();
|
||||
|
||||
URI put = storageInterface.put(
|
||||
IdUtils.create(),
|
||||
null,
|
||||
new URI(longObjectName),
|
||||
new ByteArrayInputStream("Hello World".getBytes())
|
||||
);
|
||||
|
||||
assertThat(put.getPath(), not(longObjectName));
|
||||
String suffix = put.getPath().substring(7); // we remove the random 5 char + '-'
|
||||
assertTrue(longObjectName.endsWith(suffix));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,6 +1104,14 @@ public abstract class StorageTestSuite {
|
||||
assertThat(withMetadata.metadata()).isEqualTo(expectedMetadata);
|
||||
}
|
||||
|
||||
@Test
|
||||
void limitShouldPreserveSpecialCharts() throws IOException {
|
||||
var uri = URI.create("/%89%B4%89%B4%EC%9D%B4%EC%96%B4+%EB%A7%90+%EC%95%84%ED%8A%B8%EC%9B%8D+NP+%EC%8A%A4%ED%8C%90+%EC%9D%B8%ED%8C%85+JQ+%EB%82%A8%EC%84%B1+%EC%9D%B8%EB%B0%B4%EB%93%9C+%EB%93%9C%EB%A1%9C%EC%A6%88%2C+101470%2C+FI261DR15M001-21-1st+Fit+%28QC%29+Sample+Data+Package-en.txt");
|
||||
|
||||
var limited = storageInterface.limit(uri, 100);
|
||||
assertThat(uri.getPath()).endsWith(limited.getPath().substring(7));
|
||||
}
|
||||
|
||||
private URI putFile(String tenantId, String path) throws Exception {
|
||||
return storageInterface.put(
|
||||
tenantId,
|
||||
|
||||
996
ui/package-lock.json
generated
996
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
"@vue-flow/core": "^1.47.0",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.13.1",
|
||||
"axios": "^1.13.2",
|
||||
"bootstrap": "^5.3.8",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.5.1",
|
||||
@@ -39,7 +39,7 @@
|
||||
"cytoscape": "^3.33.0",
|
||||
"dagre": "^0.8.5",
|
||||
"el-table-infinite-scroll": "^3.0.7",
|
||||
"element-plus": "2.11.5",
|
||||
"element-plus": "2.11.7",
|
||||
"humanize-duration": "^3.33.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -57,16 +57,16 @@
|
||||
"moment-timezone": "^0.5.46",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^5.4.296",
|
||||
"pinia": "^3.0.3",
|
||||
"posthog-js": "^1.281.0",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"pinia": "^3.0.4",
|
||||
"posthog-js": "^1.289.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"semver": "^7.7.3",
|
||||
"shiki": "^3.12.2",
|
||||
"vue": "^3.5.22",
|
||||
"shiki": "^3.15.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-gtag": "^3.6.2",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-gtag": "^3.6.3",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-material-design-icons": "^5.3.1",
|
||||
"vue-router": "^4.6.3",
|
||||
@@ -80,10 +80,10 @@
|
||||
"devDependencies": {
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@rushstack/eslint-patch": "^1.14.1",
|
||||
"@shikijs/markdown-it": "^3.14.0",
|
||||
"@shikijs/markdown-it": "^3.15.0",
|
||||
"@storybook/addon-themes": "^9.1.16",
|
||||
"@storybook/addon-vitest": "^9.1.16",
|
||||
"@storybook/test-runner": "^0.23.0",
|
||||
@@ -91,13 +91,13 @@
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/moment": "^2.11.29",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/node": "^24.10.0",
|
||||
"@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.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.1",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
@@ -107,42 +107,42 @@
|
||||
"@vueuse/router": "^14.0.0",
|
||||
"change-case": "5.4.4",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-storybook": "^9.1.16",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^27.0.1",
|
||||
"jsdom": "^27.1.0",
|
||||
"lint-staged": "^16.2.6",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "5.3.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"playwright": "^1.55.0",
|
||||
"prettier": "^3.6.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rolldown-vite": "^7.1.20",
|
||||
"rimraf": "^6.1.0",
|
||||
"rolldown-vite": "^7.2.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sass": "^1.92.3",
|
||||
"sass": "^1.93.3",
|
||||
"storybook": "^9.1.16",
|
||||
"storybook-vue3-router": "^6.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"typescript-eslint": "^8.46.3",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.1.2"
|
||||
"vue-tsc": "^3.1.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.25.11",
|
||||
"@esbuild/darwin-x64": "^0.25.11",
|
||||
"@esbuild/linux-x64": "^0.25.11",
|
||||
"@esbuild/darwin-arm64": "^0.25.12",
|
||||
"@esbuild/darwin-x64": "^0.25.12",
|
||||
"@esbuild/linux-x64": "^0.25.12",
|
||||
"@rollup/rollup-darwin-arm64": "^4.52.5",
|
||||
"@rollup/rollup-darwin-x64": "^4.52.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.52.5",
|
||||
"@swc/core-darwin-arm64": "^1.14.0",
|
||||
"@swc/core-darwin-x64": "^1.14.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.14.0"
|
||||
"@swc/core-darwin-arm64": "^1.15.0",
|
||||
"@swc/core-darwin-x64": "^1.15.0",
|
||||
"@swc/core-linux-x64-gnu": "^1.15.0"
|
||||
},
|
||||
"overrides": {
|
||||
"bootstrap": {
|
||||
|
||||
@@ -37,138 +37,137 @@
|
||||
ref="tabContent"
|
||||
:is="activeTab.component"
|
||||
:namespace="namespaceToForward"
|
||||
@go-to-detail="blueprintId => selectedBlueprintId = blueprintId"
|
||||
@go-to-detail="(blueprintId: string) => selectedBlueprintId = blueprintId"
|
||||
:embed="activeTab.props && activeTab.props.embed !== undefined ? activeTab.props.embed : true"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, onMounted, nextTick, useAttrs} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import EnterpriseBadge from "./EnterpriseBadge.vue";
|
||||
import BlueprintDetail from "./flows/blueprints/BlueprintDetail.vue";
|
||||
|
||||
export default {
|
||||
components: {EnterpriseBadge,BlueprintDetail},
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
top: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* The active embedded tab. If this component is not embedded, keep it undefined.
|
||||
*/
|
||||
embedActiveTab: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
/**
|
||||
* Especially useful when embedded since you need to handle the embedActiveTab prop change on the parent component.
|
||||
* @property {Object} newTab the new active tab
|
||||
*/
|
||||
"changed"
|
||||
],
|
||||
data() {
|
||||
interface Tab {
|
||||
name?: string;
|
||||
title: string;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
props?: any;
|
||||
count?: number;
|
||||
locked?: boolean;
|
||||
query?: any;
|
||||
component?: any;
|
||||
maximized?: boolean;
|
||||
"v-on"?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs: Tab[];
|
||||
routeName?: string;
|
||||
top?: boolean;
|
||||
/**
|
||||
* The active embedded tab. If this component is not embedded, keep it undefined.
|
||||
*/
|
||||
embedActiveTab?: string;
|
||||
namespace?: string | null;
|
||||
type?: string;
|
||||
}>(), {
|
||||
routeName: "",
|
||||
top: true,
|
||||
embedActiveTab: undefined,
|
||||
namespace: null,
|
||||
type: undefined
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* Especially useful when embedded since you need to handle the embedActiveTab prop change on the parent component.
|
||||
* @property {Object} newTab the new active tab
|
||||
*/
|
||||
changed: [tab: Tab];
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
const route = useRoute();
|
||||
|
||||
const activeName = ref<string | undefined>(undefined);
|
||||
const selectedBlueprintId = ref<string | undefined>(undefined);
|
||||
|
||||
const activeTab = computed(() => {
|
||||
return props.tabs.filter(tab => (props.embedActiveTab ?? route?.params?.tab) === tab.name)[0] || props.tabs[0];
|
||||
});
|
||||
|
||||
const isEditorActiveTab = computed(() => {
|
||||
const TAB = activeTab.value.name;
|
||||
const ROUTE = route?.name as string;
|
||||
|
||||
if (["flows/update", "flows/create"].includes(ROUTE)) {
|
||||
return TAB === "edit";
|
||||
} else if (["namespaces/update", "namespaces/create"].includes(ROUTE)) {
|
||||
if (TAB === "files") return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const attrsWithoutClass = computed(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(attrs)
|
||||
.filter(([key]) => key !== "class")
|
||||
);
|
||||
});
|
||||
|
||||
const namespaceToForward = computed(() => {
|
||||
return activeTab.value.props?.namespace ?? props.namespace;
|
||||
// in the special case of Namespace creation on Namespaces page, the tabs are loaded before the namespace creation
|
||||
// in this case this.props.namespace will be used
|
||||
});
|
||||
|
||||
const containerClass = computed(() => getTabClasses(activeTab.value));
|
||||
|
||||
const embeddedTabChange = (tab: Tab) => {
|
||||
emit("changed", tab);
|
||||
};
|
||||
|
||||
const setActiveName = () => {
|
||||
activeName.value = activeTab.value.name || "default";
|
||||
};
|
||||
|
||||
const to = (tab: Tab) => {
|
||||
if (activeTab.value === tab) {
|
||||
setActiveName();
|
||||
return route;
|
||||
} else {
|
||||
return {
|
||||
activeName: undefined,
|
||||
selectedBlueprintId : undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.setActiveName();
|
||||
},
|
||||
activeTab() {
|
||||
this.$nextTick(() => {
|
||||
this.setActiveName();
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setActiveName();
|
||||
},
|
||||
methods: {
|
||||
embeddedTabChange(tab) {
|
||||
this.$emit("changed", tab);
|
||||
},
|
||||
setActiveName() {
|
||||
this.activeName = this.activeTab.name || "default";
|
||||
},
|
||||
click(tab) {
|
||||
this.$router.push(this.to(this.tabs.filter(value => value.name === tab)[0]));
|
||||
},
|
||||
to(tab) {
|
||||
if (this.activeTab === tab) {
|
||||
this.setActiveName()
|
||||
return this.$route;
|
||||
} else {
|
||||
return {
|
||||
name: this.routeName || this.$route.name,
|
||||
params: {...this.$route.params, tab: tab.name},
|
||||
query: {...tab.query}
|
||||
};
|
||||
}
|
||||
},
|
||||
getTabClasses(tab) {
|
||||
if(tab.locked) return {"px-0": true};
|
||||
return {"container": true, "mt-4": true};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return this.getTabClasses(this.activeTab);
|
||||
},
|
||||
activeTab() {
|
||||
return this.tabs
|
||||
.filter(tab => (this.embedActiveTab ?? this.$route.params.tab) === tab.name)[0] || this.tabs[0];
|
||||
},
|
||||
isEditorActiveTab() {
|
||||
const TAB = this.activeTab.name;
|
||||
const ROUTE = this.$route.name;
|
||||
|
||||
if (["flows/update", "flows/create"].includes(ROUTE)) {
|
||||
return TAB === "edit";
|
||||
} else if (
|
||||
["namespaces/update", "namespaces/create"].includes(ROUTE)
|
||||
) {
|
||||
if (TAB === "files") return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
// Those are passed to the rendered component
|
||||
// We need to exclude class as it's already applied to this component root div
|
||||
attrsWithoutClass() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.$attrs)
|
||||
.filter(([key]) => key !== "class")
|
||||
);
|
||||
},
|
||||
namespaceToForward(){
|
||||
return this.activeTab.props?.namespace ?? this.namespace;
|
||||
// in the special case of Namespace creation on Namespaces page, the tabs are loaded before the namespace creation
|
||||
// in this case this.props.namespace will be used
|
||||
}
|
||||
name: props.routeName || route?.name,
|
||||
params: {...route?.params, tab: tab.name},
|
||||
query: {...tab.query}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClasses = (tab: Tab) => {
|
||||
if (tab.locked) return {"px-0": true};
|
||||
return {"container": true, "mt-4": true};
|
||||
};
|
||||
|
||||
if (route) {
|
||||
watch(route, () => {
|
||||
setActiveName();
|
||||
});
|
||||
}
|
||||
|
||||
watch(activeTab, () => {
|
||||
nextTick(() => {
|
||||
setActiveName();
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setActiveName();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -373,7 +373,6 @@
|
||||
import SelectTable from "../layout/SelectTable.vue";
|
||||
import TriggerAvatar from "../flows/TriggerAvatar.vue";
|
||||
import KSFilter from "../filter/components/KSFilter.vue";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
|
||||
@@ -474,8 +473,6 @@
|
||||
.filter(Boolean) as ColumnConfig[]
|
||||
);
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl();
|
||||
|
||||
const loadData = (callback?: () => void) => {
|
||||
const query = loadQuery({
|
||||
size: parseInt(String(route.query?.size ?? "25")),
|
||||
@@ -501,8 +498,7 @@
|
||||
|
||||
const {ready, onSort, onPageChanged, queryWithFilter, load} = useDataTableActions({
|
||||
dataTableRef: dataTable,
|
||||
loadData,
|
||||
saveRestoreUrl
|
||||
loadData
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<EmptyTemplate>
|
||||
<EmptyTemplate class="demo-layout">
|
||||
<img :src="image.source" :alt="image.alt" class="img">
|
||||
<div class="message-block">
|
||||
<div class="enterprise-tag">
|
||||
@@ -45,6 +45,12 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@kestra-io/ui-libs/src/scss/color-palette.scss";
|
||||
@import "@kestra-io/ui-libs/src/scss/_variables.scss";
|
||||
|
||||
.demo-layout {
|
||||
padding: $spacer 0 !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 253px;
|
||||
@@ -59,8 +65,10 @@
|
||||
}
|
||||
|
||||
.message-block {
|
||||
width: 665px;
|
||||
width: 100%;
|
||||
max-width: 665px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.enterprise-tag::before,
|
||||
.enterprise-tag::after{
|
||||
@@ -68,7 +76,6 @@
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.enterprise-tag::before{
|
||||
@@ -97,11 +104,12 @@
|
||||
.enterprise-tag{
|
||||
position: relative;
|
||||
background: $base-gray-200;
|
||||
padding: .125rem 1rem;
|
||||
border-radius: 1rem;
|
||||
padding: .125rem 0.5rem;
|
||||
border-radius: $border-radius;
|
||||
display: inline-block;
|
||||
z-index: 2;
|
||||
margin: 0 auto;
|
||||
font-size: 0.75rem;
|
||||
html.dark &{
|
||||
background: #FBFBFB26;
|
||||
}
|
||||
@@ -144,36 +152,38 @@
|
||||
html.dark &{
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg-block {
|
||||
text-align: left;
|
||||
width: 665px;
|
||||
margin: 0 auto;
|
||||
h2 {
|
||||
margin: 1.5rem 0;
|
||||
line-height: 30px;
|
||||
font-size: 20px;
|
||||
.msg-block {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
max-width: 665px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
h2 {
|
||||
margin: 1rem 0;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
border-radius: 8px;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
overflow: hidden;
|
||||
margin: 1rem auto;
|
||||
margin: $spacer auto;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
@@ -186,5 +196,74 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 60%;
|
||||
height: auto;
|
||||
margin-bottom: -1.5rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.message-block,
|
||||
.msg-block {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.enterprise-tag {
|
||||
padding: .125rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.msg-block {
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.enterprise-tag {
|
||||
font-size: 0.875rem;
|
||||
padding: .125rem 1rem;
|
||||
}
|
||||
|
||||
.msg-block {
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 253px;
|
||||
height: 212px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.msg-block {
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
import {useExecutionsStore} from "../../stores/executions";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
component: string;
|
||||
execution: {
|
||||
id: string;
|
||||
@@ -93,7 +93,10 @@
|
||||
};
|
||||
};
|
||||
tooltipPosition: string;
|
||||
}>();
|
||||
}>(), {
|
||||
component: "el-button",
|
||||
tooltipPosition: "bottom"
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
follow: [];
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections ref="dashboardComponent" :dashboard="{id: 'default', charts: []}" :charts showDefault />
|
||||
<Sections ref="dashboardComponent" :dashboard="{id: 'default', charts: []}" :charts showDefault class="mb-4" />
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
@@ -70,7 +70,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
:selectable="!hidden?.includes('selection') && canCheck"
|
||||
:no-data-text="$t('no_results.executions')"
|
||||
:rowKey="(row: any) => `${row.namespace}-${row.id}`"
|
||||
:rowKey="(row: any) => row.id"
|
||||
>
|
||||
<template #select-actions>
|
||||
<BulkSelect
|
||||
@@ -384,7 +384,7 @@
|
||||
import _merge from "lodash/merge";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {ref, computed, onMounted, watch, h, useTemplateRef} from "vue";
|
||||
import {ref, computed, watch, h, useTemplateRef} from "vue";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
||||
|
||||
@@ -423,18 +423,17 @@
|
||||
import {filterValidLabels} from "./utils";
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
import {humanizeDuration, invisibleSpace} from "../../utils/filters";
|
||||
import Utils from "../../utils/utils";
|
||||
|
||||
import action from "../../models/action";
|
||||
import permission from "../../models/permission";
|
||||
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
import {useTableColumns} from "../../composables/useTableColumns";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
import {useApplyDefaultFilter} from "../filter/composables/useDefaultFilter";
|
||||
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
@@ -495,7 +494,6 @@
|
||||
const selectedStatus = ref(undefined);
|
||||
const lastRefreshDate = ref(new Date());
|
||||
const unqueueDialogVisible = ref(false);
|
||||
const isDefaultNamespaceAllow = ref(true);
|
||||
const changeStatusDialogVisible = ref(false);
|
||||
const actionOptions = ref<Record<string, any>>({});
|
||||
const dblClickRouteName = ref("executions/update");
|
||||
@@ -613,11 +611,6 @@
|
||||
const routeInfo = computed(() => ({title: t("executions")}));
|
||||
useRouteContext(routeInfo, props.embed);
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl({
|
||||
restoreUrl: true,
|
||||
isDefaultNamespaceAllow: isDefaultNamespaceAllow.value
|
||||
});
|
||||
|
||||
const dataTableRef = ref(null);
|
||||
const selectTableRef = useTemplateRef<typeof SelectTable>("selectTable");
|
||||
|
||||
@@ -633,8 +626,7 @@
|
||||
dblClickRouteName: dblClickRouteName.value,
|
||||
embed: props.embed,
|
||||
dataTableRef,
|
||||
loadData: loadData,
|
||||
saveRestoreUrl
|
||||
loadData: loadData
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -1042,29 +1034,10 @@
|
||||
emit("state-count", {runningCount, totalCount});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const query = {...route.query};
|
||||
let queryHasChanged = false;
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
||||
query["filters[scope][EQUALS]"] = "USER";
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (queryHasChanged) {
|
||||
router.replace({query});
|
||||
}
|
||||
|
||||
if (route.name === "flows/update") {
|
||||
optionalColumns.value = optionalColumns.value.
|
||||
filter(col => col.prop !== "namespace" && col.prop !== "flowId");
|
||||
}
|
||||
useApplyDefaultFilter({
|
||||
namespace: props.namespace,
|
||||
includeTimeRange: true,
|
||||
includeScope: true
|
||||
});
|
||||
|
||||
watch(isOpenLabelsModal, (opening) => {
|
||||
|
||||
@@ -593,6 +593,9 @@
|
||||
line-height: 2rem;
|
||||
color: var(--ks-content-error) !important;
|
||||
font-size: var(--font-size-sm);
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
font-weight: normal;
|
||||
@@ -600,10 +603,15 @@
|
||||
|
||||
code{
|
||||
color: var(--ks-log-content-error) !important;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
> div {
|
||||
padding-right: 3rem;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.main-icon.material-design-icon {
|
||||
@@ -628,6 +636,9 @@
|
||||
|
||||
.el-alert__description {
|
||||
color: var(--ks-content-primary);
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.el-alert__content {
|
||||
@@ -649,6 +660,8 @@
|
||||
.line {
|
||||
padding: .5rem;
|
||||
border-top: 1px solid var(--ks-log-background-error);
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -150,7 +150,7 @@ export function useExecutionRoot() {
|
||||
follow();
|
||||
window.addEventListener("popstate", follow);
|
||||
|
||||
dependenciesCount.value = (await flowStore.loadDependencies({namespace: route.params.namespace as string, id: route.params.flowId as string, subtype: "FLOW"})).count;
|
||||
dependenciesCount.value = (await flowStore.loadDependencies({namespace: route.params.namespace as string, id: route.params.flowId as string, subtype: "FLOW"}, true)).count;
|
||||
previousExecutionId.value = route.params.id as string;
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
@remove="emit('remove', $event)"
|
||||
/>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
class="close"
|
||||
:icon="Close"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-button
|
||||
v-if="!!filterKey"
|
||||
ref="buttonRef"
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
:icon="PencilOutline"
|
||||
class="edit-button"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="filter-header">
|
||||
<label class="filter-label">{{ label }}</label>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
:icon="Close"
|
||||
@click="emits('close')"
|
||||
|
||||
70
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
70
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {onMounted} from "vue";
|
||||
import {LocationQuery, useRoute, useRouter} from "vue-router";
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
import {defaultNamespace} from "../../../composables/useNamespaces";
|
||||
|
||||
interface DefaultFilterOptions {
|
||||
namespace?: string;
|
||||
includeTimeRange?: boolean;
|
||||
includeScope?: boolean;
|
||||
legacyQuery?: boolean;
|
||||
}
|
||||
|
||||
const NAMESPACE_FILTER_PREFIX = "filters[namespace]";
|
||||
const SCOPE_FILTER_PREFIX = "filters[scope]";
|
||||
const TIME_RANGE_FILTER_PREFIX = "filters[timeRange]";
|
||||
|
||||
const hasFilterKey = (query: LocationQuery, prefix: string): boolean =>
|
||||
Object.keys(query).some(key => key.startsWith(prefix));
|
||||
|
||||
export function applyDefaultFilters(
|
||||
currentQuery: LocationQuery,
|
||||
options: DefaultFilterOptions & {
|
||||
configuration?: any;
|
||||
route?: any
|
||||
} = {}): { query: LocationQuery; hasChanges: boolean } {
|
||||
|
||||
const {configuration, route, namespace, includeTimeRange, includeScope, legacyQuery = false} = options;
|
||||
|
||||
const hasTimeRange = configuration && route
|
||||
? configuration.keys?.some((k: any) => k.key === "timeRange") ?? false
|
||||
: includeTimeRange ?? false;
|
||||
const hasScope = configuration && route
|
||||
? route?.name !== "logs/list" && (configuration.keys?.some((k: any) => k.key === "scope") ?? false)
|
||||
: includeScope ?? false;
|
||||
|
||||
const query = {...currentQuery};
|
||||
let hasChanges = false;
|
||||
|
||||
if (namespace === undefined && defaultNamespace() && !hasFilterKey(query, NAMESPACE_FILTER_PREFIX)) {
|
||||
query[legacyQuery ? "namespace" : `${NAMESPACE_FILTER_PREFIX}[PREFIX]`] = defaultNamespace();
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasScope && !hasFilterKey(query, SCOPE_FILTER_PREFIX)) {
|
||||
query[legacyQuery ? "scope" : `${SCOPE_FILTER_PREFIX}[EQUALS]`] = "USER";
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const TIME_FILTER_KEYS = /startDate|endDate|timeRange/;
|
||||
|
||||
if (hasTimeRange && !Object.keys(query).some(key => TIME_FILTER_KEYS.test(key))) {
|
||||
const defaultDuration = useMiscStore().configs?.chartDefaultDuration ?? "P30D";
|
||||
query[legacyQuery ? "timeRange" : `${TIME_RANGE_FILTER_PREFIX}[EQUALS]`] = defaultDuration;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return {query, hasChanges};
|
||||
}
|
||||
|
||||
export function useApplyDefaultFilter(options?: DefaultFilterOptions) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
const {query, hasChanges} = applyDefaultFilters(route.query, options);
|
||||
if (hasChanges) {
|
||||
router.replace({query});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
KV_COMPARATORS
|
||||
} from "../utils/filterTypes";
|
||||
import {usePreAppliedFilters} from "./usePreAppliedFilters";
|
||||
import {applyDefaultFilters} from "./useDefaultFilter";
|
||||
|
||||
export function useFilters(configuration: FilterConfiguration, showSearchInput = true, legacyQuery = false) {
|
||||
const router = useRouter();
|
||||
@@ -28,8 +29,7 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
const {
|
||||
markAsPreApplied,
|
||||
hasPreApplied,
|
||||
getPreApplied,
|
||||
getAllPreApplied
|
||||
getPreApplied
|
||||
} = usePreAppliedFilters();
|
||||
|
||||
const appendQueryParam = (query: Record<string, any>, key: string, value: string) => {
|
||||
@@ -367,13 +367,10 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
updateRoute();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets all filters to their pre-applied state and clears the search query
|
||||
*/
|
||||
const resetToPreApplied = () => {
|
||||
appliedFilters.value = getAllPreApplied();
|
||||
const defaultQuery = applyDefaultFilters({}, {configuration, route, legacyQuery}).query;
|
||||
searchQuery.value = "";
|
||||
updateRoute();
|
||||
router.push({query: defaultQuery});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const useNamespacesFilter = (): ComputedRef<FilterConfiguration> => compu
|
||||
const {t} = useI18n();
|
||||
|
||||
return {
|
||||
title: t("filter.titles.namespaces_filters"),
|
||||
title: t("filter.titles.namespace_filters"),
|
||||
searchPlaceholder: t("filter.search_placeholders.search_namespaces"),
|
||||
keys: [],
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h6>{{ t("filter.customize columns") }}</h6>
|
||||
<small>{{ t("filter.drag to reorder columns") }}</small>
|
||||
</div>
|
||||
<el-button type="text" :icon="Close" @click="$emit('close')" size="small" class="close-icon" />
|
||||
<el-button link :icon="Close" @click="$emit('close')" size="small" class="close-icon" />
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<small>{{ t("filter.select filter") }}</small>
|
||||
</div>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
:icon="Close"
|
||||
@click="$emit('close')"
|
||||
size="small"
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="default"
|
||||
:icon="isSelected(key) ? undefined : Plus"
|
||||
:class="isSelected(key) ? 'selected' : 'unselected'"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ $t("filter.saved filters") }}
|
||||
</h6>
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
:icon="Close"
|
||||
@click="$emit('close')"
|
||||
size="small"
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="action-buttons">
|
||||
<el-tooltip :content="$t('filter.edit filter')" placement="top" effect="light">
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
class="edit-button"
|
||||
:icon="PencilOutline"
|
||||
@@ -37,7 +37,7 @@
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('filter.delete filter')" placement="top" effect="light">
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="small"
|
||||
class="delete-button"
|
||||
:icon="Delete"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
:namespace="flowStore.flow?.namespace"
|
||||
:flowId="flowStore.flow?.id"
|
||||
:topbar="false"
|
||||
:restoreUrl="false"
|
||||
filter
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -249,8 +249,8 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, useTemplateRef} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {ref, computed, useTemplateRef} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import _merge from "lodash/merge";
|
||||
import * as FILTERS from "../../utils/filters";
|
||||
@@ -284,7 +284,6 @@
|
||||
import permission from "../../models/permission";
|
||||
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
@@ -294,7 +293,7 @@
|
||||
import {useTableColumns} from "../../composables/useTableColumns";
|
||||
import {DataTableRef, useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
|
||||
import {useApplyDefaultFilter} from "../filter/composables/useDefaultFilter";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
topbar?: boolean;
|
||||
@@ -312,7 +311,6 @@
|
||||
const miscStore = useMiscStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const {t} = useI18n();
|
||||
const toast = useToast()
|
||||
@@ -497,6 +495,11 @@
|
||||
updateVisibleColumns(newColumns);
|
||||
}
|
||||
|
||||
useApplyDefaultFilter({
|
||||
namespace: props.namespace,
|
||||
includeScope: true
|
||||
});
|
||||
|
||||
function exportFlows() {
|
||||
toast.confirm(
|
||||
t("flow export", {flowCount: queryBulkAction.value ? flowStore.total : selection.value.length}),
|
||||
@@ -633,25 +636,6 @@
|
||||
operation: "EQUALS"
|
||||
}];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const query = {...route.query};
|
||||
const queryKeys = Object.keys(query);
|
||||
let queryHasChanged = false;
|
||||
|
||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
||||
query["filters[scope][EQUALS]"] = "USER";
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (queryHasChanged) router.replace({query});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
import DataTable from "../layout/DataTable.vue";
|
||||
import SearchField from "../layout/SearchField.vue";
|
||||
import NamespaceSelect from "../namespaces/components/NamespaceSelect.vue";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
|
||||
@@ -77,11 +76,9 @@
|
||||
}));
|
||||
|
||||
useRouteContext(routeInfo);
|
||||
const {saveRestoreUrl} = useRestoreUrl({restoreUrl: true, isDefaultNamespaceAllow: true});
|
||||
|
||||
const {onPageChanged, onDataTableValue, queryWithFilter, ready} = useDataTableActions({
|
||||
loadData,
|
||||
saveRestoreUrl
|
||||
loadData
|
||||
});
|
||||
|
||||
const namespace = computed({
|
||||
|
||||
@@ -59,12 +59,12 @@
|
||||
const {t} = useI18n();
|
||||
|
||||
const exportYaml = () => {
|
||||
const src = flowStore.flowYaml
|
||||
if(!src) {
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([src], {type: "text/yaml"});
|
||||
localUtils.downloadUrl(window.URL.createObjectURL(blob), "flow.yaml");
|
||||
if(!flowStore.flow || !flowStore.flowYaml) return;
|
||||
|
||||
const {id, namespace} = flowStore.flow;
|
||||
const blob = new Blob([flowStore.flowYaml], {type: "text/yaml"});
|
||||
|
||||
localUtils.downloadUrl(window.URL.createObjectURL(blob), `${namespace}.${id}.yaml`);
|
||||
};
|
||||
|
||||
const flowStore = useFlowStore();
|
||||
|
||||
@@ -272,7 +272,6 @@
|
||||
import DataTable from "../layout/DataTable.vue";
|
||||
import _merge from "lodash/merge";
|
||||
import {type DataTableRef, useDataTableActions} from "../../composables/useDataTableActions.ts";
|
||||
|
||||
const dataTable = useTemplateRef<DataTableRef>("dataTable");
|
||||
|
||||
const loadData = async (callback?: () => void) => {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="text"
|
||||
link
|
||||
size="default"
|
||||
:icon="isVisible(column) ? EyeOutline : EyeOffOutline"
|
||||
:class="isVisible(column) ? 'selected' : 'unselected'"
|
||||
|
||||
@@ -28,16 +28,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onUpdated,
|
||||
ref,
|
||||
computed, h
|
||||
} from "vue";
|
||||
import {onUpdated, ref, computed, h, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
import {useMediaQuery} from "@vueuse/core";
|
||||
import {SidebarMenu} from "vue-sidebar-menu";
|
||||
|
||||
import StarOutline from "vue-material-design-icons/StarOutline.vue";
|
||||
|
||||
import Environment from "./Environment.vue";
|
||||
@@ -124,6 +119,14 @@
|
||||
});
|
||||
|
||||
const collapsed = ref(localStorage.getItem("menuCollapsed") === "true")
|
||||
|
||||
const isSmallScreen = useMediaQuery("(max-width: 768px)")
|
||||
|
||||
watch(() => $route.name, (newRoute, oldRoute) => {
|
||||
if (newRoute !== oldRoute && isSmallScreen.value && !collapsed.value) {
|
||||
onToggleCollapse(true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -161,6 +161,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-title {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to left, var(--ks-background-card), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.6;
|
||||
display: flex !important;
|
||||
@@ -212,7 +227,14 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0.4rem 0.75rem;
|
||||
|
||||
.mycontainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, auto));
|
||||
@@ -227,6 +249,8 @@
|
||||
}
|
||||
}
|
||||
@media (max-width: 664px) {
|
||||
padding: 0.3rem 0.5rem;
|
||||
|
||||
.mycontainer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, auto));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TopNavBar v-if="!embed" :title="routeInfo.title" />
|
||||
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
||||
<div class="log-content">
|
||||
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="pageSize" :page="pageNumber" :embed="embed">
|
||||
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="internalPageSize" :page="internalPageNumber" :embed="embed">
|
||||
<template #navbar v-if="!embed || showFilters">
|
||||
<KSFilter
|
||||
:configuration="logFilter"
|
||||
@@ -15,12 +15,12 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" showDefault />
|
||||
<Sections ref="dashboardRef" :charts :dashboard="{id: 'default', charts: []}" showDefault class="mb-4" />
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<div v-loading="isLoading">
|
||||
<div v-if="logsStore.logs !== undefined && logsStore.logs.length > 0" class="logs-wrapper">
|
||||
<div v-if="logsStore.logs !== undefined && logsStore.logs?.length > 0" class="logs-wrapper">
|
||||
<LogLine
|
||||
v-for="(log, i) in logsStore.logs"
|
||||
:key="`${log.taskRunId}-${i}`"
|
||||
@@ -42,6 +42,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, watch} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import _merge from "lodash/merge";
|
||||
import moment from "moment";
|
||||
import {useLogFilter} from "../filter/configurations";
|
||||
import KSFilter from "../filter/components/KSFilter.vue";
|
||||
import Sections from "../dashboard/sections/Sections.vue";
|
||||
@@ -49,193 +54,151 @@
|
||||
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
||||
import LogLine from "../logs/LogLine.vue";
|
||||
import NoData from "../layout/NoData.vue";
|
||||
|
||||
const logFilter = useLogFilter();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {mapStores} from "pinia";
|
||||
import RouteContext from "../../mixins/routeContext";
|
||||
import RestoreUrl from "../../mixins/restoreUrl";
|
||||
import DataTableActions from "../../mixins/dataTableActions";
|
||||
import _merge from "lodash/merge";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {decodeSearchParams} from "../filter/utils/helpers";
|
||||
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";
|
||||
import {defineComponent} from "vue";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
||||
props: {
|
||||
logLevel: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
embed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showFilters: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
reloadLogs: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDefaultNamespaceAllow: true,
|
||||
task: undefined,
|
||||
isLoading: false,
|
||||
lastRefreshDate: new Date(),
|
||||
canAutoRefresh: false,
|
||||
showChart: localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
storageKeys() {
|
||||
return storageKeys
|
||||
},
|
||||
...mapStores(useLogsStore),
|
||||
routeInfo() {
|
||||
return {
|
||||
title: this.$t("logs"),
|
||||
};
|
||||
},
|
||||
isFlowEdit() {
|
||||
return this.$route.name === "flows/update"
|
||||
},
|
||||
isNamespaceEdit() {
|
||||
return this.$route.name === "namespaces/update"
|
||||
},
|
||||
selectedLogLevel() {
|
||||
const decodedParams = decodeSearchParams(this.$route.query);
|
||||
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
||||
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
||||
return this.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
},
|
||||
endDate() {
|
||||
if (this.$route.query.endDate) {
|
||||
return this.$route.query.endDate;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
startDate() {
|
||||
// we mention the last refresh date here to trick
|
||||
// VueJs fine grained reactivity system and invalidate
|
||||
// computed property startDate
|
||||
if (this.$route.query.startDate && this.lastRefreshDate) {
|
||||
return this.$route.query.startDate;
|
||||
}
|
||||
if (this.$route.query.timeRange) {
|
||||
return this.$moment().subtract(this.$moment.duration(this.$route.query.timeRange).as("milliseconds")).toISOString(true);
|
||||
}
|
||||
const props = withDefaults(defineProps<{
|
||||
logLevel?: string;
|
||||
embed?: boolean;
|
||||
showFilters?: boolean;
|
||||
filters?: Record<string, any>;
|
||||
reloadLogs?: number;
|
||||
}>(), {
|
||||
embed: false,
|
||||
showFilters: false,
|
||||
filters: undefined,
|
||||
logLevel: undefined,
|
||||
reloadLogs: undefined
|
||||
});
|
||||
|
||||
// the default is PT30D
|
||||
return this.$moment().subtract(7, "days").toISOString(true);
|
||||
},
|
||||
namespace() {
|
||||
return this.$route.params.namespace ?? this.$route.params.id;
|
||||
},
|
||||
flowId() {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
charts() {
|
||||
return [
|
||||
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
||||
];
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to: any, _: any, next: (route?: any) => void) {
|
||||
const query = {...to.query};
|
||||
let queryHasChanged = false;
|
||||
const route = useRoute();
|
||||
const {t} = useI18n();
|
||||
const logsStore = useLogsStore();
|
||||
const logFilter = useLogFilter();
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
const routeInfo = computed(() => ({
|
||||
title: t("logs"),
|
||||
}));
|
||||
useRouteContext(routeInfo, props.embed);
|
||||
|
||||
if (queryHasChanged) {
|
||||
next({
|
||||
...to,
|
||||
query,
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showStatChart() {
|
||||
return this.showChart;
|
||||
},
|
||||
onShowChartChange(value: boolean) {
|
||||
this.showChart = value;
|
||||
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
||||
if (this.showStatChart()) {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
this.lastRefreshDate = new Date();
|
||||
if (this.$refs.dashboard) {
|
||||
this.$refs.dashboard.refreshCharts();
|
||||
}
|
||||
this.load();
|
||||
},
|
||||
loadQuery(base: any) {
|
||||
let queryFilter = this.filters ?? this.queryWithFilter();
|
||||
const isLoading = ref(false);
|
||||
const lastRefreshDate = ref(new Date());
|
||||
const showChart = ref(localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false");
|
||||
const dashboardRef = ref();
|
||||
|
||||
if (this.isFlowEdit) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
||||
queryFilter["filters[flowId][EQUALS]"] = this.flowId;
|
||||
} else if (this.isNamespaceEdit) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
||||
}
|
||||
const isFlowEdit = computed(() => route.name === "flows/update");
|
||||
const isNamespaceEdit = computed(() => route.name === "namespaces/update");
|
||||
const selectedLogLevel = computed(() => {
|
||||
const decodedParams = decodeSearchParams(route.query);
|
||||
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
||||
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
||||
return props.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
});
|
||||
const endDate = computed(() => {
|
||||
if (route.query.endDate) {
|
||||
return route.query.endDate;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const startDate = computed(() => {
|
||||
// we mention the last refresh date here to trick
|
||||
// VueJs fine grained reactivity system and invalidate
|
||||
// computed property startDate
|
||||
if (route.query.startDate && lastRefreshDate.value) {
|
||||
return route.query.startDate;
|
||||
}
|
||||
if (route.query.timeRange) {
|
||||
return moment().subtract(moment.duration(route.query.timeRange as string).as("milliseconds")).toISOString(true);
|
||||
}
|
||||
|
||||
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
||||
queryFilter["startDate"] = this.startDate;
|
||||
queryFilter["endDate"] = this.endDate;
|
||||
}
|
||||
// the default is PT30D
|
||||
return moment().subtract(7, "days").toISOString(true);
|
||||
});
|
||||
const flowId = computed(() => route.params.id);
|
||||
const namespace = computed(() => route.params.namespace ?? route.params.id);
|
||||
const charts = computed(() => [
|
||||
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
||||
]);
|
||||
|
||||
delete queryFilter["level"];
|
||||
const loadQuery = (base: any) => {
|
||||
let queryFilter = props.filters ?? queryWithFilter();
|
||||
|
||||
return _merge(base, queryFilter)
|
||||
},
|
||||
load() {
|
||||
this.isLoading = true
|
||||
if (isFlowEdit.value) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||
queryFilter["filters[flowId][EQUALS]"] = flowId.value;
|
||||
} else if (isNamespaceEdit.value) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||
}
|
||||
|
||||
const data = {
|
||||
page: this.filters ? this.internalPageNumber : this.$route.query.page || this.internalPageNumber,
|
||||
size: this.filters ? this.internalPageSize : this.$route.query.size || this.internalPageSize,
|
||||
...this.filters
|
||||
};
|
||||
this.logsStore.findLogs(this.loadQuery({
|
||||
...data,
|
||||
minLevel: this.filters ? null : this.selectedLogLevel,
|
||||
sort: "timestamp:desc"
|
||||
}))
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
this.saveRestoreUrl();
|
||||
});
|
||||
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
||||
queryFilter["startDate"] = startDate.value;
|
||||
queryFilter["endDate"] = endDate.value;
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
reloadLogs(newValue) {
|
||||
if(newValue) this.refresh();
|
||||
},
|
||||
delete queryFilter["level"];
|
||||
|
||||
return _merge(base, queryFilter);
|
||||
};
|
||||
|
||||
const loadData = (callback?: () => void) => {
|
||||
isLoading.value = true;
|
||||
|
||||
const data = {
|
||||
page: props.filters ? internalPageNumber.value : route.query.page || internalPageNumber.value,
|
||||
size: props.filters ? internalPageSize.value : route.query.size || internalPageSize.value,
|
||||
...props.filters
|
||||
};
|
||||
|
||||
logsStore.findLogs(loadQuery({
|
||||
...data,
|
||||
minLevel: props.filters ? null : selectedLogLevel.value,
|
||||
sort: "timestamp:desc"
|
||||
}))
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
if (callback) callback();
|
||||
});
|
||||
};
|
||||
|
||||
const {onPageChanged, queryWithFilter, internalPageNumber, internalPageSize} = useDataTableActions({
|
||||
loadData
|
||||
});
|
||||
|
||||
const showStatChart = () => showChart.value;
|
||||
|
||||
const onShowChartChange = (value: boolean) => {
|
||||
showChart.value = value;
|
||||
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
||||
if (showStatChart()) {
|
||||
loadData();
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
lastRefreshDate.value = new Date();
|
||||
if (dashboardRef.value) {
|
||||
dashboardRef.value.refreshCharts();
|
||||
}
|
||||
loadData();
|
||||
};
|
||||
|
||||
watch(() => route.query, () => {
|
||||
loadData();
|
||||
}, {deep: true});
|
||||
|
||||
watch(() => props.reloadLogs, (newValue) => {
|
||||
if (newValue) refresh();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Load data on mount if not embedded
|
||||
if (!props.embed) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onBeforeMount} from "vue";
|
||||
import {ref, computed, onBeforeMount, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {isEntryAPluginElementPredicate, TaskIcon} from "@kestra-io/ui-libs";
|
||||
import DottedLayout from "../layout/DottedLayout.vue";
|
||||
@@ -71,6 +71,7 @@
|
||||
import headerImage from "../../assets/icons/plugin.svg";
|
||||
import headerImageDark from "../../assets/icons/plugin-dark.svg";
|
||||
import {usePluginsStore} from "../../stores/plugins";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -85,13 +86,23 @@
|
||||
embed: false
|
||||
});
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl();
|
||||
|
||||
const icons = ref<Record<string, any>>({});
|
||||
const searchText = ref("");
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
searchText.value = query;
|
||||
const newQuery: Record<string, any> = {...route.query};
|
||||
if (query !== undefined && query !== null && String(query).trim() !== "") {
|
||||
newQuery.q = query;
|
||||
} else {
|
||||
// remove an empty `q=` in the URL on plugins/view
|
||||
delete newQuery.q;
|
||||
}
|
||||
|
||||
router.push({
|
||||
query: {...route.query, q: query || undefined}
|
||||
query: newQuery
|
||||
});
|
||||
};
|
||||
|
||||
@@ -177,6 +188,11 @@
|
||||
loadPluginIcons();
|
||||
searchText.value = String(route.query?.q ?? "");
|
||||
});
|
||||
|
||||
watch(() => route.query.q, (newQ) => {
|
||||
searchText.value = String(newQ ?? "");
|
||||
saveRestoreUrl();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
height: 300px;
|
||||
overflow: visible;
|
||||
direction: rtl;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +140,9 @@
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--ks-border-primary);
|
||||
@@ -152,5 +154,68 @@
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
padding: 0 4rem;
|
||||
|
||||
.header-block {
|
||||
.img-wrapper {
|
||||
width: 250px;
|
||||
height: 214px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
padding: 0 2rem;
|
||||
|
||||
.header-block {
|
||||
.d-flex.flex-row {
|
||||
flex-direction: column !important;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.d-flex.flex-column {
|
||||
align-items: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.img-wrapper {
|
||||
width: 200px;
|
||||
height: 171px;
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 1.5rem;
|
||||
|
||||
.header-block {
|
||||
|
||||
p {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
padding: 0 1rem;
|
||||
|
||||
.header-block {
|
||||
|
||||
h5 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useRoute, useRouter} from "vue-router";
|
||||
import _merge from "lodash/merge";
|
||||
import _cloneDeep from "lodash/cloneDeep";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
import useRestoreUrl from "./useRestoreUrl";
|
||||
|
||||
interface SortItem {
|
||||
prop?: string;
|
||||
@@ -26,7 +27,6 @@ interface DataTableActionsOptions {
|
||||
embed?: boolean;
|
||||
dataTableRef?: Ref<DataTableRef | null>;
|
||||
loadData?: (callback?: () => void) => void;
|
||||
saveRestoreUrl?: () => void;
|
||||
}
|
||||
|
||||
export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
@@ -35,7 +35,6 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
|
||||
const sort = ref("");
|
||||
const dblClickRouteName = ref(options.dblClickRouteName);
|
||||
const loadInit = ref(true);
|
||||
const ready = ref(false);
|
||||
const internalPageSize = ref(25);
|
||||
const internalPageNumber = ref(1);
|
||||
@@ -47,6 +46,8 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
const embed = computed(() => options.embed);
|
||||
const dataTableRef = computed(() => options.dataTableRef?.value);
|
||||
|
||||
const {loadInit, saveRestoreUrl} = useRestoreUrl({restoreUrl: true});
|
||||
|
||||
const sortString = (sortItem: SortItem, sortKeyMapper: (k: string) => string): string | undefined => {
|
||||
if (sortItem && sortItem.prop && sortItem.order) {
|
||||
return `${sortKeyMapper(sortItem.prop)}:${sortItem.order === "descending" ? "desc" : "asc"}`;
|
||||
@@ -149,9 +150,7 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
ready.value = true;
|
||||
loadInit.value = true;
|
||||
|
||||
if (options.saveRestoreUrl) {
|
||||
options.saveRestoreUrl();
|
||||
}
|
||||
saveRestoreUrl();
|
||||
|
||||
if (dataTableRef.value) {
|
||||
dataTableRef.value.isLoading = false;
|
||||
|
||||
@@ -47,6 +47,11 @@ export default function useRestoreUrl(options: UseRestoreUrlOptions = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges saved URL query parameters from sessionStorage with current route.
|
||||
* Only adds missing parameters to avoid overwriting user changes.
|
||||
* Updates route only when changes are made.
|
||||
*/
|
||||
const goToRestoreUrl = () => {
|
||||
if (!restoreUrl) {
|
||||
return;
|
||||
@@ -84,9 +89,12 @@ export default function useRestoreUrl(options: UseRestoreUrlOptions = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically call goToRestoreUrl on mount if needed (equivalent to created() hook)
|
||||
/**
|
||||
* Automatically restores saved URL state from sessionStorage on mount.
|
||||
* Only triggers when restoreUrl is enabled and saved state exists.
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (Object.keys(route.query).length === 0 && restoreUrl) {
|
||||
if (restoreUrl && localStorageValue.value) {
|
||||
loadInit.value = false;
|
||||
goToRestoreUrl();
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@
|
||||
import {useDocStore} from "../../../../stores/doc";
|
||||
import {canCreate} from "override/composables/blueprintsPermissions";
|
||||
import {useDataTableActions} from "../../../../composables/useDataTableActions";
|
||||
import useRestoreUrl from "../../../../composables/useRestoreUrl";
|
||||
import {useBlueprintFilter} from "../../../../components/filter/configurations";
|
||||
|
||||
const blueprintFilter = useBlueprintFilter();
|
||||
@@ -128,8 +127,6 @@
|
||||
|
||||
const {onPageChanged, onDataLoaded, load, ready, internalPageNumber, internalPageSize} = useDataTableActions({loadData});
|
||||
|
||||
useRestoreUrl();
|
||||
|
||||
const emit = defineEmits(["goToDetail", "loaded"]);
|
||||
|
||||
const route = useRoute();
|
||||
@@ -273,15 +270,13 @@
|
||||
docStore.docId = `blueprints.${props.blueprintType}`;
|
||||
});
|
||||
|
||||
watch(route,
|
||||
(newValue, oldValue) => {
|
||||
if (oldValue.name === newValue.name) {
|
||||
selectedTags.value = initSelectedTags();
|
||||
searchText.value = route.query.q || "";
|
||||
load(onDataLoaded);
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(route, (newRoute, oldRoute) => {
|
||||
if (newRoute.name === oldRoute.name) {
|
||||
selectedTags.value = initSelectedTags();
|
||||
searchText.value = newRoute.query.q || "";
|
||||
load(onDataLoaded);
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchText, () => {
|
||||
load(onDataLoaded);
|
||||
|
||||
@@ -89,11 +89,14 @@
|
||||
import permission from "../../../models/permission";
|
||||
import action from "../../../models/action";
|
||||
|
||||
import useRestoreUrl from "../../../composables/useRestoreUrl";
|
||||
|
||||
import DotsSquare from "vue-material-design-icons/DotsSquare.vue";
|
||||
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
|
||||
const namespacesFilter = useNamespacesFilter();
|
||||
const {saveRestoreUrl} = useRestoreUrl({restoreUrl: true});
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
@@ -127,8 +130,12 @@
|
||||
|
||||
onMounted(() => loadData());
|
||||
watch(
|
||||
() => route.query,
|
||||
() => loadData(),
|
||||
() => route.query.q,
|
||||
() => {
|
||||
loadData();
|
||||
saveRestoreUrl();
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
const miscStore = useMiscStore();
|
||||
|
||||
@@ -7,21 +7,23 @@ import DemoAuditLogs from "../components/demo/AuditLogs.vue"
|
||||
import DemoInstance from "../components/demo/Instance.vue"
|
||||
import DemoApps from "../components/demo/Apps.vue"
|
||||
import DemoTests from "../components/demo/Tests.vue"
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
import {applyDefaultFilters} from "../components/filter/composables/useDefaultFilter";
|
||||
|
||||
function maybeAddTimeRangeFilter(to) {
|
||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||
|
||||
// Default to the configured duration if no time range is set
|
||||
if (!Object.keys(to.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||
const miscStore = useMiscStore();
|
||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D"; // Fallback to 30 days
|
||||
to.query["filters[timeRange][EQUALS]"] = defaultDuration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
export function applyBeforeEnterFilter(options) {
|
||||
return (to, _from, next) => {
|
||||
const {query, hasChanges} = applyDefaultFilters(to.query, options);
|
||||
|
||||
if (hasChanges) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
@@ -35,15 +37,6 @@ export default [
|
||||
path: "/:tenant?/dashboards/:dashboard?",
|
||||
component: () => import("../components/dashboard/Dashboard.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!to.params.dashboard) {
|
||||
next({
|
||||
name: "home",
|
||||
@@ -53,16 +46,21 @@ export default [
|
||||
},
|
||||
query: to.query,
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
applyBeforeEnterFilter({includeTimeRange: true, includeScope: false})(to, from, next);
|
||||
},
|
||||
},
|
||||
{name: "dashboards/create", path: "/:tenant?/dashboards/new", component: () => import("../components/dashboard/components/Create.vue")},
|
||||
{name: "dashboards/update", path: "/:tenant?/dashboards/:dashboard/edit", component: () => import("override/components/dashboard/Edit.vue")},
|
||||
|
||||
//Flows
|
||||
{name: "flows/list", path: "/:tenant?/flows", component: () => import("../components/flows/Flows.vue")},
|
||||
{
|
||||
name: "flows/list",
|
||||
path: "/:tenant?/flows",
|
||||
component: () => import("../components/flows/Flows.vue"),
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: false, includeScope: true}),
|
||||
},
|
||||
{name: "flows/search", path: "/:tenant?/flows/search", component: () => import("../components/flows/FlowsSearch.vue")},
|
||||
{name: "flows/create", path: "/:tenant?/flows/new", component: () => import("../components/flows/FlowCreate.vue")},
|
||||
{name: "flows/update", path: "/:tenant?/flows/edit/:namespace/:id/:tab?", component: () => import("../components/flows/FlowRoot.vue")},
|
||||
@@ -72,18 +70,7 @@ export default [
|
||||
name: "executions/list",
|
||||
path: "/:tenant?/executions",
|
||||
component: () => import("../components/executions/Executions.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: true, includeScope: true}),
|
||||
},
|
||||
{name: "executions/update", path: "/:tenant?/executions/:namespace/:flowId/:id/:tab?", component: () => import("../components/executions/ExecutionRoot.vue")},
|
||||
|
||||
@@ -111,18 +98,7 @@ export default [
|
||||
name: "logs/list",
|
||||
path: "/:tenant?/logs",
|
||||
component: () => import("../components/logs/LogsWrapper.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: true, includeScope: false}),
|
||||
},
|
||||
|
||||
//Namespaces
|
||||
|
||||
@@ -439,7 +439,7 @@ export const useFlowStore = defineStore("flow", () => {
|
||||
}
|
||||
|
||||
function loadDependencies(options: { namespace: string, id: string, subtype: "FLOW" | "EXECUTION" }, onlyCount = false) {
|
||||
return axios.get(`${apiUrl()}/flows/${options.namespace}/${options.id}/dependencies?expandAll=true`).then(response => {
|
||||
return axios.get(`${apiUrl()}/flows/${options.namespace}/${options.id}/dependencies?expandAll=${onlyCount ? false : true}`).then(response => {
|
||||
return {
|
||||
...(!onlyCount ? {data: transformResponse(response.data, options.subtype)} : {}),
|
||||
count: response.data.nodes ? new Set(response.data.nodes.map((r:{uid:string}) => r.uid)).size : 0
|
||||
|
||||
@@ -18,7 +18,6 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
@@ -55,20 +54,15 @@ class BasicAuthServiceTest {
|
||||
@Inject
|
||||
private InstanceService instanceService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
stubFor(any(urlMatching(".*"))
|
||||
.willReturn(aResponse()
|
||||
.withStatus(404)
|
||||
.withBody("No stub matched")));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
void stopApp() {
|
||||
stubFor(
|
||||
post(urlEqualTo("/v1/reports/events"))
|
||||
.willReturn(aResponse().withStatus(200))
|
||||
);
|
||||
deleteSetting();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void isBasicAuthInitialized(){
|
||||
deleteSetting();
|
||||
@@ -76,22 +70,22 @@ class BasicAuthServiceTest {
|
||||
new BasicAuthConfiguration(USER_NAME, PASSWORD, null, null)
|
||||
).config;
|
||||
basicAuthService.init();
|
||||
assertThat(basicAuthService.isBasicAuthInitialized()).as("isBasicAuthInitialized after init with basic auth configured with user and password").isTrue();
|
||||
assertTrue(basicAuthService.isBasicAuthInitialized());
|
||||
|
||||
deleteSetting();
|
||||
assertThat(basicAuthService.isBasicAuthInitialized()).as("not isBasicAuthInitialized when there is no settings").isFalse();
|
||||
assertFalse(basicAuthService.isBasicAuthInitialized());
|
||||
|
||||
basicAuthService.basicAuthConfiguration = new ConfigWrapper(
|
||||
new BasicAuthConfiguration(USER_NAME, null, null, null)
|
||||
).config;
|
||||
basicAuthService.init();
|
||||
assertThat(basicAuthService.isBasicAuthInitialized()).as("not isBasicAuthInitialized when there is settings but only user name").isFalse();
|
||||
assertFalse(basicAuthService.isBasicAuthInitialized());
|
||||
|
||||
basicAuthService.basicAuthConfiguration = new ConfigWrapper(
|
||||
new BasicAuthConfiguration(null, null, null, null)
|
||||
).config;
|
||||
basicAuthService.init();
|
||||
assertThat(basicAuthService.isBasicAuthInitialized()).as("not isBasicAuthInitialized when there is settings but no user and password").isFalse();
|
||||
assertFalse(basicAuthService.isBasicAuthInitialized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -214,11 +208,6 @@ class BasicAuthServiceTest {
|
||||
|
||||
@Test
|
||||
void initFromYamlConfig() throws TimeoutException {
|
||||
stubFor(
|
||||
post(urlEqualTo("/v1/reports/events"))
|
||||
.willReturn(aResponse().withStatus(200))
|
||||
);
|
||||
|
||||
basicAuthService.basicAuthConfiguration = basicAuthConfiguration;
|
||||
basicAuthService.init();
|
||||
assertConfigurationMatchesApplicationYaml();
|
||||
@@ -248,11 +237,6 @@ class BasicAuthServiceTest {
|
||||
|
||||
@Test
|
||||
void saveValidAuthConfig() throws TimeoutException {
|
||||
stubFor(
|
||||
post(urlEqualTo("/v1/reports/events"))
|
||||
.willReturn(aResponse().withStatus(200))
|
||||
);
|
||||
|
||||
basicAuthService.save(new BasicAuthCredentials(null, USER_NAME, PASSWORD));
|
||||
awaitOssAuthEventApiCall(USER_NAME);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user