mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
30 Commits
dependabot
...
v0.23.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e51b100b0 | ||
|
|
bc81e01608 | ||
|
|
9f2162c942 | ||
|
|
97992d99ee | ||
|
|
f90f6b8429 | ||
|
|
0f7360ae81 | ||
|
|
938590f31f | ||
|
|
b2d1c84a86 | ||
|
|
d7ca302830 | ||
|
|
8656e852cc | ||
|
|
cc72336350 | ||
|
|
316d89764e | ||
|
|
4873bf4d36 | ||
|
|
204bf7f5e1 | ||
|
|
1e0950fdf8 | ||
|
|
4cddc704f4 | ||
|
|
f2f0e29f93 | ||
|
|
95011e022e | ||
|
|
65503b708a | ||
|
|
876b8cb2e6 | ||
|
|
f3b7592dfa | ||
|
|
4dbeaf86bb | ||
|
|
f98e78399d | ||
|
|
71dac0f311 | ||
|
|
3077d0ac7a | ||
|
|
9504bbaffe | ||
|
|
159c9373ad | ||
|
|
55b9088b55 | ||
|
|
601d1a0abb | ||
|
|
4a1cf98f26 |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -43,6 +43,9 @@ jobs:
|
||||
SONATYPE_GPG_KEYID: ${{ secrets.SONATYPE_GPG_KEYID }}
|
||||
SONATYPE_GPG_PASSWORD: ${{ secrets.SONATYPE_GPG_PASSWORD }}
|
||||
SONATYPE_GPG_FILE: ${{ secrets.SONATYPE_GPG_FILE }}
|
||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||
|
||||
|
||||
end:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
6
.github/workflows/setversion-tag.yml
vendored
6
.github/workflows/setversion-tag.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
||||
echo "Invalid release version. Must match regex: ^[0-9]+(\.[0-9]+)(\.[0-9]+)-(rc[0-9])?(-SNAPSHOT)?$"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Extract the major and minor versions
|
||||
BASE_VERSION=$(echo "$RELEASE_VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\..*/\1/')
|
||||
RELEASE_BRANCH="refs/heads/releases/v${BASE_VERSION}.x"
|
||||
|
||||
|
||||
CURRENT_BRANCH="$GITHUB_REF"
|
||||
if ! [[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]]; then
|
||||
echo "Invalid release branch. Expected $RELEASE_BRANCH, was $CURRENT_BRANCH"
|
||||
@@ -54,4 +54,4 @@ jobs:
|
||||
git commit -m"chore(version): update to version '$RELEASE_VERSION'"
|
||||
git push
|
||||
git tag -a "v$RELEASE_VERSION" -m"v$RELEASE_VERSION"
|
||||
git push origin "v$RELEASE_VERSION"
|
||||
git push --tags
|
||||
38
.github/workflows/workflow-github-release.yml
vendored
38
.github/workflows/workflow-github-release.yml
vendored
@@ -6,23 +6,15 @@ on:
|
||||
GH_PERSONAL_TOKEN:
|
||||
description: "The Github personal token."
|
||||
required: true
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
SLACK_RELEASES_WEBHOOK_URL:
|
||||
description: "The Slack webhook URL."
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Github - Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Download Exec
|
||||
- name: Artifacts - Download executable
|
||||
uses: actions/download-artifact@v4
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
name: exe
|
||||
path: build/executable
|
||||
|
||||
# Check out
|
||||
- name: Checkout - Repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -36,11 +28,20 @@ jobs:
|
||||
with:
|
||||
repository: kestra-io/actions
|
||||
sparse-checkout-cone-mode: true
|
||||
ref: fix/core-release
|
||||
path: actions
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
|
||||
# Download Exec
|
||||
# Must be done after checkout actions
|
||||
- name: Artifacts - Download executable
|
||||
uses: actions/download-artifact@v4
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
name: exe
|
||||
path: build/executable
|
||||
|
||||
|
||||
# GitHub Release
|
||||
- name: Create GitHub release
|
||||
uses: ./actions/.github/actions/github-release
|
||||
@@ -49,3 +50,16 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||
|
||||
# Trigger gha workflow to bump helm chart version
|
||||
- name: GitHub - Trigger the Helm chart version bump
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
repository: kestra-io/helm-charts
|
||||
event-type: update-helm-chart-version
|
||||
client-payload: |-
|
||||
{
|
||||
"new_version": "${{ github.ref_name }}",
|
||||
"github_repository": "${{ github.repository }}",
|
||||
"github_actor": "${{ github.actor }}"
|
||||
}
|
||||
9
.github/workflows/workflow-release.yml
vendored
9
.github/workflows/workflow-release.yml
vendored
@@ -42,6 +42,12 @@ on:
|
||||
SONATYPE_GPG_FILE:
|
||||
description: "The Sonatype GPG file."
|
||||
required: true
|
||||
GH_PERSONAL_TOKEN:
|
||||
description: "The Github personal token."
|
||||
required: true
|
||||
SLACK_RELEASES_WEBHOOK_URL:
|
||||
description: "The Slack webhook URL."
|
||||
required: true
|
||||
jobs:
|
||||
build-artifacts:
|
||||
name: Build - Artifacts
|
||||
@@ -77,4 +83,5 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/workflow-github-release.yml
|
||||
secrets:
|
||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
GH_PERSONAL_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
|
||||
SLACK_RELEASES_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
|
||||
1
.plugins
1
.plugins
@@ -61,6 +61,7 @@
|
||||
#plugin-jenkins:io.kestra.plugin:plugin-jenkins:LATEST
|
||||
#plugin-jira:io.kestra.plugin:plugin-jira:LATEST
|
||||
#plugin-kafka:io.kestra.plugin:plugin-kafka:LATEST
|
||||
#plugin-kestra:io.kestra.plugin:plugin-kestra:LATEST
|
||||
#plugin-kubernetes:io.kestra.plugin:plugin-kubernetes:LATEST
|
||||
#plugin-langchain4j:io.kestra.plugin:plugin-langchain4j:LATEST
|
||||
#plugin-ldap:io.kestra.plugin:plugin-ldap:LATEST
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package io.kestra.core.metrics;
|
||||
|
||||
import io.kestra.core.models.ServerType;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import io.micronaut.configuration.metrics.aggregator.MeterRegistryConfigurer;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.micronaut.context.annotation.Value;
|
||||
import io.micronaut.core.annotation.Nullable;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
|
||||
@@ -15,20 +18,26 @@ public class GlobalTagsConfigurer implements MeterRegistryConfigurer<SimpleMeter
|
||||
@Inject
|
||||
MetricConfig metricConfig;
|
||||
|
||||
@Nullable
|
||||
@Value("${kestra.server-type}")
|
||||
ServerType serverType;
|
||||
|
||||
@Override
|
||||
public void configure(SimpleMeterRegistry meterRegistry) {
|
||||
if (metricConfig.getTags() != null) {
|
||||
meterRegistry
|
||||
.config()
|
||||
.commonTags(
|
||||
metricConfig.getTags()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.flatMap(e -> Stream.of(e.getKey(), e.getValue()))
|
||||
.toList()
|
||||
.toArray(String[]::new)
|
||||
);
|
||||
}
|
||||
String[] tags = Stream
|
||||
.concat(
|
||||
metricConfig.getTags() != null ? metricConfig.getTags()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.flatMap(e -> Stream.of(e.getKey(), e.getValue())) : Stream.empty(),
|
||||
serverType != null ? Stream.of("server_type", serverType.name()) : Stream.empty()
|
||||
)
|
||||
.toList()
|
||||
.toArray(String[]::new);
|
||||
|
||||
meterRegistry
|
||||
.config()
|
||||
.commonTags(tags);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -329,6 +329,14 @@ public class DefaultPluginRegistry implements PluginRegistry {
|
||||
pluginClassByIdentifier.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
**/
|
||||
@Override
|
||||
public boolean isVersioningSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public record PluginBundleIdentifier(@Nullable URL location) {
|
||||
|
||||
public static PluginBundleIdentifier CORE = new PluginBundleIdentifier(null);
|
||||
|
||||
@@ -116,4 +116,11 @@ public interface PluginRegistry {
|
||||
default void clear() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether plugin-versioning is supported by this registry.
|
||||
*
|
||||
* @return {@code true} if supported. Otherwise {@code false}.
|
||||
*/
|
||||
boolean isVersioningSupported();
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
||||
DeserializationContext context) throws IOException {
|
||||
Class<? extends Plugin> pluginType = null;
|
||||
|
||||
final String identifier = extractPluginRawIdentifier(node);
|
||||
final String identifier = extractPluginRawIdentifier(node, pluginRegistry.isVersioningSupported());
|
||||
if (identifier != null) {
|
||||
log.trace("Looking for Plugin for: {}",
|
||||
identifier
|
||||
@@ -103,7 +103,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
||||
);
|
||||
|
||||
if (DataChart.class.isAssignableFrom(pluginType)) {
|
||||
final Class<? extends Plugin> dataFilterClass = pluginRegistry.findClassByIdentifier(extractPluginRawIdentifier(node.get("data")));
|
||||
final Class<? extends Plugin> dataFilterClass = pluginRegistry.findClassByIdentifier(extractPluginRawIdentifier(node.get("data"), pluginRegistry.isVersioningSupported()));
|
||||
ParameterizedType genericDataFilterClass = (ParameterizedType) dataFilterClass.getGenericSuperclass();
|
||||
Type dataFieldsEnum = genericDataFilterClass.getActualTypeArguments()[0];
|
||||
TypeFactory typeFactory = JacksonMapper.ofJson().getTypeFactory();
|
||||
@@ -142,7 +142,7 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
||||
);
|
||||
}
|
||||
|
||||
static String extractPluginRawIdentifier(final JsonNode node) {
|
||||
static String extractPluginRawIdentifier(final JsonNode node, final boolean isVersioningSupported) {
|
||||
String type = Optional.ofNullable(node.get(TYPE)).map(JsonNode::textValue).orElse(null);
|
||||
String version = Optional.ofNullable(node.get(VERSION)).map(JsonNode::textValue).orElse(null);
|
||||
|
||||
@@ -150,6 +150,6 @@ public final class PluginDeserializer<T extends Plugin> extends JsonDeserializer
|
||||
return null;
|
||||
}
|
||||
|
||||
return version != null && !version.isEmpty() ? type + ":" + version : type;
|
||||
return isVersioningSupported && version != null && !version.isEmpty() ? type + ":" + version : type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.amazon.ion.IonSystem;
|
||||
import com.amazon.ion.system.*;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.StreamReadConstraints;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@@ -36,6 +37,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static com.fasterxml.jackson.core.StreamReadConstraints.DEFAULT_MAX_STRING_LEN;
|
||||
|
||||
public final class JacksonMapper {
|
||||
public static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<>() {};
|
||||
public static final TypeReference<List<Object>> LIST_TYPE_REFERENCE = new TypeReference<>() {};
|
||||
@@ -43,6 +46,12 @@ public final class JacksonMapper {
|
||||
|
||||
private JacksonMapper() {}
|
||||
|
||||
static {
|
||||
StreamReadConstraints.overrideDefaultStreamReadConstraints(
|
||||
StreamReadConstraints.builder().maxNameLength(DEFAULT_MAX_STRING_LEN).build()
|
||||
);
|
||||
}
|
||||
|
||||
private static final ObjectMapper MAPPER = JacksonMapper.configure(
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ public class FlowService {
|
||||
previous :
|
||||
FlowWithSource.of(flowToImport.toBuilder().revision(previous.getRevision() + 1).build(), source)
|
||||
)
|
||||
.orElseGet(() -> FlowWithSource.of(flowToImport, source).toBuilder().revision(1).build());
|
||||
.orElseGet(() -> FlowWithSource.of(flowToImport, source).toBuilder().tenantId(tenantId).revision(1).build());
|
||||
} else {
|
||||
return maybeExisting
|
||||
.map(previous -> repository().update(flow, previous))
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.kestra.core.models.Plugin;
|
||||
import io.kestra.core.plugins.PluginRegistry;
|
||||
@@ -15,12 +16,14 @@ import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginDeserializerTest {
|
||||
|
||||
@Mock
|
||||
private PluginRegistry registry;
|
||||
|
||||
|
||||
@Test
|
||||
void shouldSucceededDeserializePluginGivenValidType() throws JsonProcessingException {
|
||||
// Given
|
||||
@@ -38,8 +41,9 @@ class PluginDeserializerTest {
|
||||
|
||||
TestPluginHolder deserialized = om.readValue(input, TestPluginHolder.class);
|
||||
// Then
|
||||
Assertions.assertEquals(TestPlugin.class.getCanonicalName(), deserialized.plugin().getType());
|
||||
Mockito.verify(registry, Mockito.only()).findClassByIdentifier(identifier);
|
||||
assertThat(TestPlugin.class.getCanonicalName()).isEqualTo(deserialized.plugin().getType());
|
||||
Mockito.verify(registry, Mockito.times(1)).isVersioningSupported();
|
||||
Mockito.verify(registry, Mockito.times(1)).findClassByIdentifier(identifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -57,17 +61,33 @@ class PluginDeserializerTest {
|
||||
});
|
||||
|
||||
// Then
|
||||
Assertions.assertEquals("io.kestra.core.plugins.serdes.Unknown", exception.getTypeId());
|
||||
assertThat("io.kestra.core.plugins.serdes.Unknown").isEqualTo(exception.getTypeId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullPluginIdentifierGivenNullType() {
|
||||
Assertions.assertNull(PluginDeserializer.extractPluginRawIdentifier(new TextNode(null)));
|
||||
assertThat(PluginDeserializer.extractPluginRawIdentifier(new TextNode(null), true)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullPluginIdentifierGivenEmptyType() {
|
||||
Assertions.assertNull(PluginDeserializer.extractPluginRawIdentifier(new TextNode("")));
|
||||
assertThat(PluginDeserializer.extractPluginRawIdentifier(new TextNode(""), true)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTypeWithVersionGivenSupportedVersionTrue() {
|
||||
ObjectNode jsonNodes = new ObjectNode(new ObjectMapper().getNodeFactory());
|
||||
jsonNodes.set("type", new TextNode("io.kestra.core.plugins.serdes.Unknown"));
|
||||
jsonNodes.set("version", new TextNode("1.0.0"));
|
||||
assertThat(PluginDeserializer.extractPluginRawIdentifier(jsonNodes, true)).isEqualTo("io.kestra.core.plugins.serdes.Unknown:1.0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTypeWithVersionGivenSupportedVersionFalse() {
|
||||
ObjectNode jsonNodes = new ObjectNode(new ObjectMapper().getNodeFactory());
|
||||
jsonNodes.set("type", new TextNode("io.kestra.core.plugins.serdes.Unknown"));
|
||||
jsonNodes.set("version", new TextNode("1.0.0"));
|
||||
assertThat(PluginDeserializer.extractPluginRawIdentifier(jsonNodes, false)).isEqualTo("io.kestra.core.plugins.serdes.Unknown");
|
||||
}
|
||||
|
||||
public record TestPluginHolder(Plugin plugin) {
|
||||
|
||||
200
dev-tools/update-plugin-kestra-version.sh
Executable file
200
dev-tools/update-plugin-kestra-version.sh
Executable file
@@ -0,0 +1,200 @@
|
||||
#!/bin/bash
|
||||
#===============================================================================
|
||||
# SCRIPT: update-plugin-kestra-version.sh
|
||||
#
|
||||
# DESCRIPTION:
|
||||
# This script can be used to update the gradle 'kestraVersion' property on each kestra plugin repository.
|
||||
# By default, if no `GITHUB_PAT` environment variable exist, the script will attempt to clone GitHub repositories using SSH_KEY.
|
||||
#
|
||||
#USAGE:
|
||||
# ./dev-tools/update-plugin-kestra-version.sh --branch <branch> --version <version> [plugin-repositories...]
|
||||
#
|
||||
#OPTIONS:
|
||||
# --branch <branch> Specify the branch on which to update the kestraCoreVersion (default: master).
|
||||
# --version <version> Specify the Kestra core version (required).
|
||||
# --plugin-file File containing the plugin list (default: .plugins)
|
||||
# --dry-run Specify to run in DRY_RUN.
|
||||
# -y, --yes Automatically confirm prompts (non-interactive).
|
||||
# -h, --help Show this help message and exit.
|
||||
|
||||
|
||||
# EXAMPLES:
|
||||
# To release all plugins:
|
||||
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)"
|
||||
# To release a specific plugin:
|
||||
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)" plugin-kubernetes
|
||||
# To release specific plugins from file:
|
||||
# ./update-plugin-kestra-version.sh --branch=releases/v0.23.x --version="[0.23,0.24)" --plugin-file .plugins
|
||||
#===============================================================================
|
||||
|
||||
set -e;
|
||||
|
||||
###############################################################
|
||||
# Global vars
|
||||
###############################################################
|
||||
BASEDIR=$(dirname "$(readlink -f $0)")
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
SCRIPT_NAME="${SCRIPT_NAME%.*}"
|
||||
WORKING_DIR="/tmp/kestra-$SCRIPT_NAME-$(date +%s)"
|
||||
PLUGIN_FILE="$BASEDIR/../.plugins"
|
||||
GIT_BRANCH=master
|
||||
|
||||
###############################################################
|
||||
# Functions
|
||||
###############################################################
|
||||
|
||||
# Function to display the help message
|
||||
usage() {
|
||||
echo "Usage: $0 --branch <branch> --version <version> [plugin-repositories...]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --branch <branch> Specify the branch on which to update the kestraCoreVersion (default: master)."
|
||||
echo " --version <version> Specify the Kestra core version (required)."
|
||||
echo " --plugin-file File containing the plugin list (default: .plugins)"
|
||||
echo " --dry-run Specify to run in DRY_RUN."
|
||||
echo " -y, --yes Automatically confirm prompts (non-interactive)."
|
||||
echo " -h, --help Show this help message and exit."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Function to ask to continue
|
||||
function askToContinue() {
|
||||
read -p "Are you sure you want to continue? [y/N] " confirm
|
||||
[[ "$confirm" =~ ^[Yy]$ ]] || { echo "Operation cancelled."; exit 1; }
|
||||
}
|
||||
|
||||
###############################################################
|
||||
# Options
|
||||
###############################################################
|
||||
|
||||
PLUGINS_ARGS=()
|
||||
AUTO_YES=false
|
||||
DRY_RUN=false
|
||||
# Get the options
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--branch)
|
||||
GIT_BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--branch=*)
|
||||
GIT_BRANCH="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version=*)
|
||||
VERSION="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--plugin-file)
|
||||
PLUGIN_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--plugin-file=*)
|
||||
PLUGIN_FILE="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-y|--yes)
|
||||
AUTO_YES=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
PLUGINS_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
## Check options
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo -e "Missing required argument: --version\n";
|
||||
usage
|
||||
fi
|
||||
|
||||
## Get plugin list
|
||||
if [[ "${#PLUGINS_ARGS[@]}" -eq 0 ]]; then
|
||||
if [ -f "$PLUGIN_FILE" ]; then
|
||||
PLUGINS=$(cat "$PLUGIN_FILE" | grep "io\\.kestra\\." | sed -e '/#/s/^.//' | cut -d':' -f1 | uniq | sort);
|
||||
PLUGINS_COUNT=$(echo "$PLUGINS" | wc -l);
|
||||
PLUGINS_ARRAY=$(echo "$PLUGINS" | xargs || echo '');
|
||||
PLUGINS_ARRAY=($PLUGINS_ARRAY);
|
||||
fi
|
||||
else
|
||||
PLUGINS_ARRAY=("${PLUGINS_ARGS[@]}")
|
||||
PLUGINS_COUNT="${#PLUGINS_ARGS[@]}"
|
||||
fi
|
||||
|
||||
|
||||
## Get plugin list
|
||||
echo "VERSION=$RELEASE_VERSION"
|
||||
echo "GIT_BRANCH=$GIT_BRANCH"
|
||||
echo "DRY_RUN=$DRY_RUN"
|
||||
echo "Found ($PLUGINS_COUNT) plugin repositories:";
|
||||
|
||||
for PLUGIN in "${PLUGINS_ARRAY[@]}"; do
|
||||
echo "$PLUGIN"
|
||||
done
|
||||
|
||||
if [[ "$AUTO_YES" == false ]]; then
|
||||
askToContinue
|
||||
fi
|
||||
|
||||
###############################################################
|
||||
# Main
|
||||
###############################################################
|
||||
mkdir -p $WORKING_DIR
|
||||
|
||||
COUNTER=1;
|
||||
for PLUGIN in "${PLUGINS_ARRAY[@]}"
|
||||
do
|
||||
cd $WORKING_DIR;
|
||||
|
||||
echo "---------------------------------------------------------------------------------------"
|
||||
echo "[$COUNTER/$PLUGINS_COUNT] Update Plugin: $PLUGIN"
|
||||
echo "---------------------------------------------------------------------------------------"
|
||||
if [[ -z "${GITHUB_PAT}" ]]; then
|
||||
git clone git@github.com:kestra-io/$PLUGIN
|
||||
else
|
||||
echo "Clone git repository using GITHUB PAT"
|
||||
git clone https://${GITHUB_PAT}@github.com/kestra-io/$PLUGIN.git
|
||||
fi
|
||||
cd "$PLUGIN";
|
||||
|
||||
if [[ "$PLUGIN" == "plugin-transform" ]] && [[ "$GIT_BRANCH" == "master" ]]; then # quickfix
|
||||
git checkout main;
|
||||
else
|
||||
git checkout "$GIT_BRANCH";
|
||||
fi
|
||||
|
||||
CURRENT_BRANCH=$(git branch --show-current);
|
||||
|
||||
echo "Update kestraVersion for plugin: $PLUGIN on branch $CURRENT_BRANCH:";
|
||||
# Update the kestraVersion property
|
||||
sed -i "s/^kestraVersion=.*/kestraVersion=${VERSION}/" ./gradle.properties
|
||||
# Display diff
|
||||
git diff --exit-code --unified=0 ./gradle.properties | grep -E '^\+|^-' | grep -v -E '^\+\+\+|^---'
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if [[ "$AUTO_YES" == false ]]; then
|
||||
askToContinue
|
||||
fi
|
||||
git add ./gradle.properties
|
||||
git commit -m"chore(deps): update kestraVersion to ${VERSION}."
|
||||
git push
|
||||
else
|
||||
echo "Skip git commit/push [DRY_RUN=true]";
|
||||
fi
|
||||
COUNTER=$(( COUNTER + 1 ));
|
||||
done;
|
||||
|
||||
exit 0;
|
||||
@@ -1,6 +1,6 @@
|
||||
version=0.24.0-SNAPSHOT
|
||||
version=0.23.0-rc3-SNAPSHOT
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.priority=low
|
||||
org.gradle.priority=low
|
||||
|
||||
@@ -1166,7 +1166,7 @@ public class JdbcExecutor implements ExecutorInterface, Service {
|
||||
|
||||
try {
|
||||
// Handle paused tasks
|
||||
if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESUME_FLOW)) {
|
||||
if (executionDelay.getDelayType().equals(ExecutionDelay.DelayType.RESUME_FLOW) && !pair.getLeft().getState().isTerminated()) {
|
||||
FlowInterface flow = flowMetaStore.findByExecution(pair.getLeft()).orElseThrow();
|
||||
if (executionDelay.getTaskRunId() == null) {
|
||||
// if taskRunId is null, this means we restart a flow that was delayed at startup (scheduled on)
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import type {StorybookConfig} from "@storybook/vue3-vite";
|
||||
import path from "path";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../tests/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-themes",
|
||||
"@storybook/experimental-addon-test"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/vue3-vite",
|
||||
options: {},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const {default: viteJSXPlugin} = await import("@vitejs/plugin-vue-jsx")
|
||||
config.plugins = [
|
||||
...(config.plugins ?? []),
|
||||
viteJSXPlugin(),
|
||||
];
|
||||
return config;
|
||||
},
|
||||
stories: [
|
||||
"../tests/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-themes",
|
||||
"@storybook/addon-vitest",
|
||||
"@storybook/addon-docs"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/vue3-vite",
|
||||
options: {},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const {default: viteJSXPlugin} = await import("@vitejs/plugin-vue-jsx")
|
||||
config.plugins = [
|
||||
...(config.plugins ?? []),
|
||||
viteJSXPlugin(),
|
||||
];
|
||||
|
||||
if (config.resolve) {
|
||||
config.resolve.alias = {
|
||||
"override/services/filterLanguagesProvider": path.resolve(__dirname, "../tests/storybook/mocks/services/filterLanguagesProvider.mock.ts"),
|
||||
...config.resolve?.alias
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {setup} from "@storybook/vue3";
|
||||
import {setup} from "@storybook/vue3-vite";
|
||||
import {withThemeByClassName} from "@storybook/addon-themes";
|
||||
import initApp from "../src/utils/init";
|
||||
import stores from "../src/stores/store";
|
||||
@@ -11,7 +11,7 @@ window.KESTRA_BASE_PATH = "/ui";
|
||||
window.KESTRA_UI_PATH = "./";
|
||||
|
||||
/**
|
||||
* @type {import('@storybook/vue3').Preview}
|
||||
* @type {import('@storybook/vue3-vite').Preview}
|
||||
*/
|
||||
const preview = {
|
||||
parameters: {
|
||||
|
||||
39
ui/.storybook/vitest.config.js
Normal file
39
ui/.storybook/vitest.config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import path from "node:path";
|
||||
|
||||
import {defineProject, mergeConfig} from "vitest/config";
|
||||
|
||||
import {storybookTest} from "@storybook/addon-vitest/vitest-plugin";
|
||||
import initialConfig from "../vite.config.js"
|
||||
|
||||
|
||||
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
|
||||
export default mergeConfig(
|
||||
// We need to define a side first project to set up the alias for the filterLanguagesProvider mock because otherwise the `override` alias will take precedence over this one (first match rule)
|
||||
defineProject({
|
||||
resolve: {
|
||||
alias: {
|
||||
"override/services/filterLanguagesProvider": path.resolve(__dirname, "../tests/storybook/mocks/services/filterLanguagesProvider.mock.ts")
|
||||
}
|
||||
}
|
||||
}),
|
||||
mergeConfig(
|
||||
initialConfig,
|
||||
defineProject({
|
||||
plugins: [
|
||||
// The plugin will run tests for the stories defined in your Storybook config
|
||||
// See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest
|
||||
storybookTest({configDir: path.join(__dirname)}),
|
||||
],
|
||||
test: {
|
||||
name: "storybook",
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: "playwright",
|
||||
instances: [{browser: "chromium"}],
|
||||
},
|
||||
setupFiles: ["vitest.setup.ts"],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import {beforeAll} from "vitest";
|
||||
import {setProjectAnnotations} from "@storybook/vue3";
|
||||
import {setProjectAnnotations} from "@storybook/vue3-vite";
|
||||
import * as projectAnnotations from "./preview";
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
|
||||
@@ -20,7 +20,7 @@ export default [
|
||||
"**/*.spec.ts",
|
||||
"vite.config.js",
|
||||
"vitest.config.js",
|
||||
"vitest.workspace.js",
|
||||
".storybook/vitest.config.js",
|
||||
],
|
||||
languageOptions: {globals: globals.node},
|
||||
},
|
||||
|
||||
44474
ui/package-lock.json
generated
44474
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
291
ui/package.json
291
ui/package.json
@@ -1,149 +1,150 @@
|
||||
{
|
||||
"name": "kestra",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "npm@10.9.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
"test:lint": "eslint",
|
||||
"test:cicd": "vitest run --coverage",
|
||||
"test:storybook": "vitest run --project=storybook",
|
||||
"translations:check": "node ./src/translations/check.js",
|
||||
"lint": "eslint --fix",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"prepare": "cd .. && husky ui/.husky && rm -f .git/hooks/*",
|
||||
"notes": "node ./scripts/ci/generateReleaseNotes",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.5",
|
||||
"@kestra-io/ui-libs": "^0.0.203",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.44.0",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.9.0",
|
||||
"bootstrap": "^5.3.6",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"core-js": "^3.42.0",
|
||||
"cronstrue": "^2.61.0",
|
||||
"dagre": "^0.8.5",
|
||||
"el-table-infinite-scroll": "^3.0.6",
|
||||
"element-plus": "^2.9.10",
|
||||
"humanize-duration": "^3.32.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markdown-it-mark": "^4.0.0",
|
||||
"markdown-it-meta": "^0.0.1",
|
||||
"material-file-icons": "^2.4.0",
|
||||
"md5": "^2.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-range": "^4.0.2",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^5.2.133",
|
||||
"posthog-js": "^1.245.1",
|
||||
"rapidoc": "^9.3.8",
|
||||
"semver": "^7.7.2",
|
||||
"shiki": "^3.4.2",
|
||||
"splitpanes": "^3.2.0",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-gtag": "^2.1.0",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"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.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@rushstack/eslint-patch": "^1.11.0",
|
||||
"@shikijs/markdown-it": "^3.4.2",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/addon-themes": "^8.6.14",
|
||||
"@storybook/blocks": "^8.6.14",
|
||||
"@storybook/experimental-addon-test": "^8.6.14",
|
||||
"@storybook/test": "^8.6.14",
|
||||
"@storybook/test-runner": "^0.22.0",
|
||||
"@storybook/vue3": "^8.6.14",
|
||||
"@storybook/vue3-vite": "^8.6.14",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.2.0",
|
||||
"@vitest/browser": "^3.1.4",
|
||||
"@vitest/coverage-v8": "^3.1.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vueuse/router": "^13.2.0",
|
||||
"change-case": "4.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"decompress": "^4.2.1",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-storybook": "^0.12.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "5.3.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sass": "^1.89.0",
|
||||
"storybook": "^8.6.14",
|
||||
"storybook-vue3-router": "^5.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/darwin-x64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.41.0",
|
||||
"@rollup/rollup-darwin-x64": "^4.41.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.41.0",
|
||||
"@swc/core-darwin-arm64": "^1.11.24",
|
||||
"@swc/core-darwin-x64": "^1.11.24",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.24"
|
||||
},
|
||||
"overrides": {
|
||||
"bootstrap": {
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
"name": "kestra",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "npm@10.9.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
"test:lint": "eslint",
|
||||
"test:cicd": "vitest run --coverage",
|
||||
"test:storybook": "vitest run --project=storybook",
|
||||
"translations:check": "node ./src/translations/check.js",
|
||||
"lint": "eslint --fix",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"prepare": "cd .. && husky ui/.husky && rm -f .git/hooks/*",
|
||||
"notes": "node ./scripts/ci/generateReleaseNotes",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"el-table-infinite-scroll": {
|
||||
"vue": "$vue"
|
||||
"dependencies": {
|
||||
"@js-joda/core": "^5.6.5",
|
||||
"@kestra-io/ui-libs": "^0.0.203",
|
||||
"@vue-flow/background": "^1.3.2",
|
||||
"@vue-flow/controls": "^1.1.2",
|
||||
"@vue-flow/core": "^1.44.0",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.9.0",
|
||||
"bootstrap": "^5.3.6",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"core-js": "^3.42.0",
|
||||
"cronstrue": "^2.61.0",
|
||||
"dagre": "^0.8.5",
|
||||
"el-table-infinite-scroll": "^3.0.6",
|
||||
"element-plus": "^2.9.10",
|
||||
"humanize-duration": "^3.32.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-anchor": "^9.2.0",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markdown-it-mark": "^4.0.0",
|
||||
"markdown-it-meta": "^0.0.1",
|
||||
"material-file-icons": "^2.4.0",
|
||||
"md5": "^2.3.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-range": "^4.0.2",
|
||||
"moment-timezone": "^0.5.48",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^5.2.133",
|
||||
"posthog-js": "^1.245.1",
|
||||
"rapidoc": "^9.3.8",
|
||||
"semver": "^7.7.2",
|
||||
"shiki": "^3.4.2",
|
||||
"splitpanes": "^3.2.0",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-gtag": "^2.1.0",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"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.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@rushstack/eslint-patch": "^1.11.0",
|
||||
"@shikijs/markdown-it": "^3.4.2",
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/addon-themes": "^9.0.5",
|
||||
"@storybook/addon-vitest": "^9.0.5",
|
||||
"@storybook/test-runner": "^0.22.0",
|
||||
"@storybook/types": "^8.6.14",
|
||||
"@storybook/vue3-vite": "^9.0.5",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
"@types/testing-library__user-event": "^4.1.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue-jsx": "^4.2.0",
|
||||
"@vitest/browser": "^3.2.3",
|
||||
"@vitest/coverage-v8": "^3.2.3",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vueuse/router": "^13.2.0",
|
||||
"change-case": "4.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"decompress": "^4.2.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-storybook": "^9.0.5",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-yaml": "5.3.1",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"sass": "^1.89.0",
|
||||
"storybook": "^9.0.5",
|
||||
"storybook-vue3-router": "^5.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/darwin-x64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.41.0",
|
||||
"@rollup/rollup-darwin-x64": "^4.41.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.41.0",
|
||||
"@swc/core-darwin-arm64": "^1.11.24",
|
||||
"@swc/core-darwin-x64": "^1.11.24",
|
||||
"@swc/core-linux-x64-gnu": "^1.11.24"
|
||||
},
|
||||
"overrides": {
|
||||
"bootstrap": {
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7"
|
||||
},
|
||||
"el-table-infinite-scroll": {
|
||||
"vue": "$vue"
|
||||
},
|
||||
"storybook": "$storybook"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,mjs,cjs,ts,vue}": "eslint --fix"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,mjs,cjs,ts,vue}": "eslint --fix"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="w-100 p-4" v-if="currentView === views.DASHBOARD">
|
||||
<ChartsSection :charts="charts.map(chart => chart.data)" />
|
||||
<ChartsSection :charts="charts.map(chart => chart.data)" show-default />
|
||||
</div>
|
||||
<div class="main-editor" v-else>
|
||||
<div
|
||||
@@ -100,7 +100,7 @@
|
||||
:source="selectedChart.content"
|
||||
:chart="selectedChart"
|
||||
:identifier="selectedChart.id"
|
||||
is-preview
|
||||
show-default
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<MonacoEditor
|
||||
ref="monacoEditor"
|
||||
class="border flex-grow-1 position-relative"
|
||||
:language="`${language?.domain === undefined ? '' : (language.domain + '-')}${legacyQuery ? 'legacy-' : ''}filter`"
|
||||
:schema-type="language?.domain"
|
||||
:language="`${language.domain === undefined ? '' : (language.domain + '-')}${legacyQuery ? 'legacy-' : ''}filter`"
|
||||
:schema-type="language.domain"
|
||||
:value="filter"
|
||||
@change="filter = $event"
|
||||
:theme="themeComputed"
|
||||
@@ -15,6 +15,7 @@
|
||||
@editor-did-mount="editorDidMount"
|
||||
suggestions-on-focus
|
||||
:placeholder="placeholder ?? t('filters.label')"
|
||||
data-testid="monaco-filter"
|
||||
/>
|
||||
<el-button-group
|
||||
class="d-inline-flex"
|
||||
@@ -84,6 +85,7 @@
|
||||
import {Comparators, getComparator} from "../../composables/monaco/languages/filters/filterCompletion.ts";
|
||||
import {watchDebounced} from "@vueuse/core";
|
||||
import {FilterLanguage} from "../../composables/monaco/languages/filters/filterLanguage.ts";
|
||||
import DefaultFilterLanguage from "../../composables/monaco/languages/filters/impl/defaultFilterLanguage.ts";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -91,7 +93,7 @@
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
prefix?: string | undefined;
|
||||
language?: FilterLanguage | undefined,
|
||||
language?: FilterLanguage,
|
||||
propertiesWidth?: number,
|
||||
buttons?: (Omit<Buttons, "settings"> & {
|
||||
settings: Omit<Buttons["settings"], "charts"> & { charts?: Buttons["settings"]["charts"] }
|
||||
@@ -104,7 +106,7 @@
|
||||
legacyQuery?: boolean,
|
||||
}>(), {
|
||||
prefix: undefined,
|
||||
language: undefined,
|
||||
language: () => DefaultFilterLanguage,
|
||||
propertiesWidth: 144,
|
||||
buttons: () => ({
|
||||
refresh: {
|
||||
@@ -146,7 +148,7 @@
|
||||
}
|
||||
}));
|
||||
|
||||
const itemsPrefix = computed(() => props.prefix ?? route.name?.toString());
|
||||
const itemsPrefix = computed(() => props.prefix ?? route.name?.toString() ?? "fallback-filters");
|
||||
|
||||
const emits = defineEmits(["dashboard", "updateProperties"]);
|
||||
|
||||
@@ -160,14 +162,9 @@
|
||||
.map(([key, value]) => [value, key])
|
||||
);
|
||||
|
||||
const EXCLUDED_QUERY_FIELDS = ["sort", "size", "page"];
|
||||
const queryParamsToKeep = ref<string[]>([]);
|
||||
|
||||
const filteredRouteQuery = computed(() => route.query === undefined
|
||||
? undefined
|
||||
: Object.fromEntries(Object.entries(route.query).filter(([key]) => !EXCLUDED_QUERY_FIELDS.includes(key))) as LocationQuery
|
||||
);
|
||||
|
||||
watch(filteredRouteQuery, (newVal) => {
|
||||
watch(() => route.query, (newVal) => {
|
||||
if (skipRouteWatcherOnce.value) {
|
||||
skipRouteWatcherOnce.value = false;
|
||||
return;
|
||||
@@ -177,12 +174,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
queryParamsToKeep.value = [];
|
||||
|
||||
let query = newVal;
|
||||
if (props.queryNamespace !== undefined) {
|
||||
query = Object.fromEntries(
|
||||
Object.entries(newVal)
|
||||
.filter(([key]) => {
|
||||
return key.startsWith(props.queryNamespace + "[");
|
||||
if (key.startsWith(props.queryNamespace + "[")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
queryParamsToKeep.value.push(key);
|
||||
return false;
|
||||
})
|
||||
.map(([key, value]) =>
|
||||
// We trim the queryNamespace from the key
|
||||
@@ -198,17 +202,28 @@
|
||||
*/
|
||||
filter.value = Object.entries(query)
|
||||
.flatMap(([key, values]) => {
|
||||
if (!props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(queryRemapper[key] ?? key))) {
|
||||
queryParamsToKeep.value.push(key);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values];
|
||||
}
|
||||
|
||||
return values.map(value => (queryRemapper?.[key] ?? key) + Comparators.EQUALS + value);
|
||||
return values.map(value => (queryRemapper[key] ?? key) + Comparators.EQUALS + value);
|
||||
}).join(" ");
|
||||
} else {
|
||||
filter.value = Object.entries(query)
|
||||
.filter(([key]) => key.startsWith("filters["))
|
||||
.flatMap(([key, values]) => {
|
||||
const [_, filterKey, comparator, subKey] = key.match(/filters\[([^\]]+)]\[([^\]]+)](?:\[([^\]]+)])?/) ?? [];
|
||||
|
||||
if (!props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(queryRemapper[filterKey] ?? filterKey))) {
|
||||
queryParamsToKeep.value.push(key);
|
||||
return [];
|
||||
}
|
||||
|
||||
let maybeSubKeyString;
|
||||
if (subKey === undefined) {
|
||||
maybeSubKeyString = "";
|
||||
@@ -220,7 +235,7 @@
|
||||
values = [values];
|
||||
}
|
||||
|
||||
return values.map(value => (queryRemapper?.[filterKey] ?? filterKey) + maybeSubKeyString + getComparator(comparator as Parameters<typeof getComparator>[0]) + value);
|
||||
return values.map(value => (queryRemapper?.[filterKey] ?? filterKey) + maybeSubKeyString + getComparator(comparator as Parameters<typeof getComparator>[0]) + (value!.includes(" ") ? `"${value}"` : value));
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
@@ -243,10 +258,10 @@
|
||||
return {};
|
||||
}
|
||||
|
||||
const KEY_MATCHER = "((?:(?!" + COMPARATORS_REGEX + ")\\S)+?)";
|
||||
const KEY_MATCHER = "((?:(?!" + COMPARATORS_REGEX + ")(?:\\S|\"[^\"]*\"))+?)";
|
||||
const COMPARATOR_MATCHER = "(" + COMPARATORS_REGEX + ")";
|
||||
const MAYBE_PREVIOUS_VALUE = "(?:(?<=\\S),)?";
|
||||
const VALUE_MATCHER = "((?:" + MAYBE_PREVIOUS_VALUE + "(?:(?:\"[^\\n,]*\")|(?:[^\\s,]*)))+)";
|
||||
const VALUE_MATCHER = "((?:" + MAYBE_PREVIOUS_VALUE + "(?:(?:\"[^\"]*\")|(?:[^\\s,]*)))+)";
|
||||
const filterMatcher = new RegExp("\\s*(?<!\\S)" +
|
||||
"((?:" + KEY_MATCHER + COMPARATOR_MATCHER + VALUE_MATCHER + ")" +
|
||||
"|\"([^\"]*)\"" +
|
||||
@@ -259,7 +274,7 @@
|
||||
|
||||
// If we're not in a {key}{comparator}{value} format, we assume it's a text search
|
||||
if (key === undefined) {
|
||||
if (props.language?.textFilterSupported && (text === undefined || !props.language?.keyMatchers()?.some(keyMatcher => keyMatcher.test(text)))) {
|
||||
if (props.language.textFilterSupported && (text === undefined || !props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(text)))) {
|
||||
filters.push({
|
||||
key: "text",
|
||||
comparator: "EQUALS",
|
||||
@@ -269,15 +284,17 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!props.language?.keyMatchers()?.some(keyMatcher => keyMatcher.test(key))) {
|
||||
if (!props.language.keyMatchers()?.some(keyMatcher => keyMatcher.test(key))) {
|
||||
continue; // Skip keys that don't match the language key matchers
|
||||
}
|
||||
|
||||
if (!props.language?.comparatorsPerKey()[FilterLanguage.withNestedKeyPlaceholder(key)].some(c => Comparators[c] === comparator)) {
|
||||
if (!props.language.comparatorsPerKey()[FilterLanguage.withNestedKeyPlaceholder(key)].some(c => Comparators[c] === comparator)) {
|
||||
continue; // Skip comparators that are not valid for the key
|
||||
}
|
||||
|
||||
const values = [...new Set(commaSeparatedValues?.split(",")?.filter(value => value !== "")?.map(value => value.replaceAll("\"", "")) ?? [])];
|
||||
const values = [...new Set(
|
||||
[...commaSeparatedValues?.matchAll(/,?(?:"([^"]*)"|([^",]+))/g) ?? []].map(([_, quotedValue, rawValue]) => quotedValue ?? rawValue) ?? [])
|
||||
];
|
||||
if (values.length === 0) {
|
||||
continue; // Skip empty values
|
||||
}
|
||||
@@ -308,9 +325,9 @@
|
||||
|
||||
if (!props.legacyQuery) {
|
||||
if (key.includes(".")) {
|
||||
const keyAndSubKeyMatch = queryKey.match(/([^.]+)\.([^.]+)/);
|
||||
const keyAndSubKeyMatch = queryKey.match(/([^.]+)\.(\S+)/);
|
||||
const rootKey = keyAndSubKeyMatch?.[1];
|
||||
const subKey = keyAndSubKeyMatch?.[2];
|
||||
const subKey = keyAndSubKeyMatch?.[2].replace(/^"([^"]*)"$/, "$1");
|
||||
if (rootKey === undefined || subKey === undefined) {
|
||||
return [];
|
||||
}
|
||||
@@ -446,13 +463,16 @@
|
||||
skipRouteWatcherOnce.value = true;
|
||||
router.push({
|
||||
query: {
|
||||
sort: route.query.sort,
|
||||
size: route.query.size,
|
||||
page: route.query.page,
|
||||
...Object.fromEntries(queryParamsToKeep.value.map(key => {
|
||||
return [
|
||||
key,
|
||||
route.query[key]
|
||||
];
|
||||
})),
|
||||
...filterQueryString.value
|
||||
}
|
||||
});
|
||||
}, {immediate: true, debounce: 500});
|
||||
}, {immediate: true, debounce: 1000});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -739,7 +739,7 @@
|
||||
);
|
||||
|
||||
if (this.namespace) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.$route.params.id || this.namespace;
|
||||
}
|
||||
|
||||
return _merge(base, queryFilter);
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
import {Moment} from "moment";
|
||||
import PlaceholderContentWidget from "../../composables/monaco/PlaceholderContentWidget.ts";
|
||||
import ICodeEditor = editor.ICodeEditor;
|
||||
import debounce from "lodash/debounce";
|
||||
import {hashCode} from "../../utils/global.ts";
|
||||
|
||||
const store = useStore();
|
||||
@@ -168,7 +169,7 @@
|
||||
base: kestraBaseTheme.base
|
||||
}
|
||||
: theme as Partial<editor.IStandaloneThemeData> & { base: editor.BuiltinTheme };
|
||||
|
||||
|
||||
const themeId = hashCode(JSON.stringify(theme)).toString();
|
||||
monaco.editor.defineTheme(themeId, {
|
||||
inherit: true,
|
||||
@@ -176,7 +177,7 @@
|
||||
colors: {},
|
||||
...base
|
||||
});
|
||||
|
||||
|
||||
return themeId;
|
||||
}
|
||||
|
||||
@@ -244,7 +245,7 @@
|
||||
watch(() => props.theme, (newTheme) => {
|
||||
if (typeof newTheme === "object") {
|
||||
const themeId = defineCustomTheme(newTheme);
|
||||
|
||||
|
||||
if (editorResolved.value) {
|
||||
monaco.editor.setTheme(themeId);
|
||||
}
|
||||
@@ -303,7 +304,7 @@
|
||||
node.querySelector(`.${KESTRA_ICON_WRAPPER_CLASS}`)?.remove();
|
||||
|
||||
if (completionValue.includes(".") && !completionValue.includes("{")) {
|
||||
if (store.state.plugin.icons[completionValue] !== undefined) {
|
||||
if (store.state.plugin?.icons?.[completionValue] !== undefined) {
|
||||
replaceRowIcon(vsCodeIcon, h(TaskIcon, {
|
||||
cls: completionValue,
|
||||
"only-icon": true,
|
||||
@@ -464,6 +465,12 @@
|
||||
(window as any).clearEditor = () => {
|
||||
localEditor?.getModel()?.setValue("")
|
||||
};
|
||||
(window as any).acceptSuggestion = () => {
|
||||
localEditor?.trigger("acceptSelectedSuggestion", "acceptSelectedSuggestion", {});
|
||||
};
|
||||
(window as any).nextSuggestion = () => {
|
||||
localEditor?.trigger("selectNextSuggestion", "selectNextSuggestion", {});
|
||||
};
|
||||
})
|
||||
|
||||
onBeforeUnmount(function () {
|
||||
@@ -657,12 +664,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
localEditor.onDidChangeCursorPosition(() => {
|
||||
localEditor.onDidChangeCursorPosition(debounce(() => {
|
||||
if (suggestController.model.state !== 0) {
|
||||
suggestController.cancelSuggestWidget();
|
||||
localEditor!.trigger("refreshSuggestionsOnCursorMove", "editor.action.triggerSuggest", {});
|
||||
}
|
||||
})
|
||||
}, 300))
|
||||
}
|
||||
|
||||
if (!props.input) {
|
||||
@@ -797,4 +804,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<component :is="autoRefresh ? 'auto-renew' : 'auto-renew-off'" class="auto-refresh-icon" />
|
||||
</kicon>
|
||||
</el-button>
|
||||
<el-button @click="triggerRefresh" data-test-id="trigger-refresh-button">
|
||||
<el-button @click="triggerRefresh" data-test-id="trigger-refresh-button" data-testid="trigger-refresh-button">
|
||||
<kicon :tooltip="$t('trigger refresh')" placement="bottom">
|
||||
<refresh />
|
||||
</kicon>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, Ref, onMounted} from "vue";
|
||||
import {ref, computed, Ref, watch, onMounted} from "vue";
|
||||
|
||||
import {useTabs} from "override/components/namespaces/useTabs";
|
||||
import {useHelpers} from "./utils/useHelpers";
|
||||
@@ -25,11 +25,17 @@
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const context = ref({title: details.title});
|
||||
const context = ref({title: details.value.title});
|
||||
useRouteContext(context);
|
||||
|
||||
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||
|
||||
watch(namespace, (newID) => {
|
||||
if (newID) {
|
||||
store.dispatch("namespace/load", newID);
|
||||
}
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
onMounted(() => {
|
||||
if (namespace.value) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component} from "vue";
|
||||
import {Component, computed, Ref} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
@@ -45,30 +45,30 @@ export function useHelpers() {
|
||||
const route = useRoute();
|
||||
const {t} = useI18n({useScope: "global"});
|
||||
|
||||
const namespace = route.params?.id as string;
|
||||
const namespace = computed(() => route.params?.id) as Ref<string>;
|
||||
|
||||
const parts = namespace?.split(".") ?? [];
|
||||
const details: Details = {
|
||||
title: parts.at(-1) || t("namespaces"),
|
||||
const parts = computed(() => namespace.value?.split(".") ?? []);
|
||||
const details: Ref<Details> = computed(() => ({
|
||||
title: parts.value.at(-1) || t("namespaces"),
|
||||
breadcrumb: [
|
||||
{label: t("namespaces"), link: {name: "namespaces/list"}},
|
||||
...parts.map((_: string, index: number) => ({
|
||||
label: parts[index],
|
||||
...parts.value.map((_: string, index: number) => ({
|
||||
label: parts.value[index],
|
||||
link: {
|
||||
name: "namespaces/update",
|
||||
params: {
|
||||
id: parts.slice(0, index + 1).join("."),
|
||||
id: parts.value.slice(0, index + 1).join("."),
|
||||
tab: "overview",
|
||||
},
|
||||
},
|
||||
disabled: index === parts.length - 1,
|
||||
disabled: index === parts.value.length - 1,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}));
|
||||
|
||||
const tabs: Tab[] = [
|
||||
// If it's a system namespace, include the blueprints tab
|
||||
...(namespace === "system"
|
||||
...(namespace.value === "system"
|
||||
? [
|
||||
{
|
||||
name: "blueprints",
|
||||
@@ -88,26 +88,34 @@ export function useHelpers() {
|
||||
name: "flows",
|
||||
title: t("flows"),
|
||||
component: Flows,
|
||||
props: {namespace, topbar: false},
|
||||
props: {namespace: namespace.value, topbar: false},
|
||||
},
|
||||
{
|
||||
name: "executions",
|
||||
title: t("executions"),
|
||||
component: Executions,
|
||||
props: {namespace, topbar: false, visibleCharts: true},
|
||||
props: {
|
||||
namespace: namespace.value,
|
||||
topbar: false,
|
||||
visibleCharts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dependencies",
|
||||
title: t("dependencies"),
|
||||
component: Dependencies,
|
||||
props: {namespace, type: "dependencies"},
|
||||
props: {namespace: namespace.value, type: "dependencies"},
|
||||
},
|
||||
{
|
||||
maximized: true,
|
||||
name: "files",
|
||||
title: t("files"),
|
||||
component: EditorView,
|
||||
props: {namespace, isNamespace: true, isReadOnly: false},
|
||||
props: {
|
||||
namespace: namespace.value,
|
||||
isNamespace: true,
|
||||
isReadOnly: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
const keyLabelToRegex = (keyLabel: string) => {
|
||||
return new RegExp(keyLabel
|
||||
.replaceAll(".", "\\.")
|
||||
.replaceAll(/\{[^}]*}/g, "(?:\"[^,\"]*\"|[^\\s,\"]*?(?=" + COMPARATORS_REGEX + "|\\s|$))"));
|
||||
.replaceAll(/\{[^}]*}/g, "(?:\"[^\"]*\"|[^\\s,\"]*?(?=" + COMPARATORS_REGEX + "|\\s|$))"));
|
||||
};
|
||||
|
||||
if (this._filterLanguage && monaco.languages.getLanguages().find(l => l.id === this.language) === undefined) {
|
||||
@@ -102,7 +102,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
includeLF: true,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/[\w."]+/, {
|
||||
[/[\w.]*(?:"[^"]*")?[\w.]*/, {
|
||||
cases: {
|
||||
...keysTokenizerCases,
|
||||
"@default": {token: "@rematch", next: "@rawText"}
|
||||
@@ -123,7 +123,7 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
],
|
||||
value: [
|
||||
[/"[^"]+(?![^"]*")/, "invalid"],
|
||||
[new RegExp("\"[^\\n,\"]*\""), {
|
||||
[new RegExp("\"[^\\n\"]*\""), {
|
||||
token: "variable.value",
|
||||
next: "@separator"
|
||||
}],
|
||||
@@ -344,4 +344,4 @@ export default class FilterLanguageConfigurator extends AbstractLanguageConfigur
|
||||
}
|
||||
})];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,19 +57,49 @@ export class YamlLanguageConfigurator extends AbstractLanguageConfigurator {
|
||||
(defaultCompletion.suggestions as {
|
||||
label: string,
|
||||
filterText: string,
|
||||
insertText: string
|
||||
insertText: string,
|
||||
sortText?: string
|
||||
}[]).forEach(suggestion => {
|
||||
if (suggestion.label.endsWith("...") && suggestion.insertText.includes(suggestion.label.substring(0, suggestion.label.length - 3))) {
|
||||
suggestion.label = suggestion.insertText;
|
||||
}
|
||||
|
||||
if (suggestion.label.includes(".")) {
|
||||
const dotSplit = suggestion.label.split(/\.(?=\w)/);
|
||||
const taskName = dotSplit.pop();
|
||||
suggestion.filterText = [taskName, ...dotSplit, taskName].join(".");
|
||||
}
|
||||
});
|
||||
const wordAtPosition = model.getWordAtPosition(position)?.word?.toLowerCase();
|
||||
if (wordAtPosition !== undefined) {
|
||||
const sortBumperText = "a1".repeat(10);
|
||||
if (suggestion.label.includes(".")) {
|
||||
const dotSplit = suggestion.label.toLowerCase().split(/\.(?=\w)/);
|
||||
if (dotSplit[dotSplit.length - 1].startsWith(wordAtPosition)) {
|
||||
suggestion.sortText = sortBumperText.repeat(5) + suggestion.label;
|
||||
} else if (dotSplit[dotSplit.length - 1].includes(wordAtPosition)) {
|
||||
suggestion.sortText = sortBumperText.repeat(4) + suggestion.label;
|
||||
} else {
|
||||
suggestion.sortText = dotSplit.splice(dotSplit.length - 1, 1).reduceRight((prefix, part) => {
|
||||
let sortBumperPrefixForPart;
|
||||
if (part.startsWith(wordAtPosition)) {
|
||||
sortBumperPrefixForPart = sortBumperText.repeat(3)
|
||||
} else if (part.includes(wordAtPosition)) {
|
||||
sortBumperPrefixForPart = sortBumperText.repeat(2);
|
||||
}
|
||||
|
||||
if (sortBumperPrefixForPart === undefined || prefix.length >= sortBumperPrefixForPart.length) {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
return sortBumperPrefixForPart;
|
||||
}, "") + suggestion.label;
|
||||
}
|
||||
|
||||
suggestion.filterText = (suggestion.label.includes(wordAtPosition) ? wordAtPosition + " " : "") + suggestion.label.toLowerCase();
|
||||
}
|
||||
|
||||
if (suggestion.sortText === undefined && suggestion.label.includes(wordAtPosition)) {
|
||||
suggestion.sortText = sortBumperText + suggestion.label;
|
||||
}
|
||||
}
|
||||
|
||||
suggestion.sortText = suggestion.sortText?.toLowerCase();
|
||||
});
|
||||
|
||||
return defaultCompletion;
|
||||
};
|
||||
@@ -258,4 +288,4 @@ export class YamlLanguageConfigurator extends AbstractLanguageConfigurator {
|
||||
|
||||
return autoCompletionProviders;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,43 @@ export default {
|
||||
actions: {
|
||||
list({commit}) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins`, {}).then(response => {
|
||||
commit("setPlugins", response.data)
|
||||
commit("setPluginSingleList", response.data.map(plugin => plugin.tasks.concat(plugin.triggers, plugin.conditions, plugin.controllers, plugin.storages, plugin.taskRunners, plugin.charts, plugin.dataFilters, plugin.aliases, plugin.logExporters)).flat())
|
||||
commit("setPlugins", response.data);
|
||||
commit("setPluginSingleList", response.data.map(plugin =>
|
||||
plugin.tasks.concat(
|
||||
plugin.triggers,
|
||||
plugin.conditions,
|
||||
plugin.controllers,
|
||||
plugin.storages,
|
||||
plugin.taskRunners,
|
||||
plugin.charts,
|
||||
plugin.dataFilters,
|
||||
plugin.aliases,
|
||||
plugin.logExporters,
|
||||
plugin.additionalPlugins
|
||||
)).flat());
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
listWithSubgroup({commit}, options) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/groups/subgroups`, {
|
||||
params: options
|
||||
}).then(response => {
|
||||
commit("setPlugins", response.data)
|
||||
commit("setPluginSingleList", response.data.map(plugin => plugin.tasks.concat(plugin.triggers, plugin.conditions, plugin.controllers, plugin.storages, plugin.taskRunners, plugin.charts, plugin.dataFilters, plugin.aliases, plugin.logExporters)).flat())
|
||||
commit("setPlugins", response.data);
|
||||
commit("setPluginSingleList", response.data.map(plugin =>
|
||||
plugin.tasks.concat(
|
||||
plugin.triggers,
|
||||
plugin.conditions,
|
||||
plugin.controllers,
|
||||
plugin.storages,
|
||||
plugin.taskRunners,
|
||||
plugin.charts,
|
||||
plugin.dataFilters,
|
||||
plugin.aliases,
|
||||
plugin.logExporters,
|
||||
plugin.additionalPlugins
|
||||
)).flat());
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
load({commit, state}, options) {
|
||||
if (options.cls === undefined) {
|
||||
@@ -63,7 +87,7 @@ export default {
|
||||
}
|
||||
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
loadVersions({commit}, options) {
|
||||
const promise = this.$http.get(
|
||||
@@ -74,7 +98,7 @@ export default {
|
||||
commit("setVersions", response.data.versions);
|
||||
}
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
icons({commit}) {
|
||||
return Promise.all([
|
||||
@@ -85,7 +109,7 @@ export default {
|
||||
|
||||
for (const [key, plugin] of Object.entries(responses[1].data)) {
|
||||
if (icons[key] === undefined) {
|
||||
icons[key] = plugin
|
||||
icons[key] = plugin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,29 +120,29 @@ export default {
|
||||
},
|
||||
groupIcons(_) {
|
||||
return Promise.all([
|
||||
this.$http.get(`${apiUrl(this)}/plugins/icons/groups`, {}),
|
||||
this.$http.get(`${apiUrl(this)}/plugins/icons/groups`, {})
|
||||
]).then(responses => {
|
||||
return responses[0].data
|
||||
return responses[0].data;
|
||||
});
|
||||
},
|
||||
loadInputsType({commit}) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/inputs`, {}).then(response => {
|
||||
commit("setInputsType", response.data)
|
||||
commit("setInputsType", response.data);
|
||||
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
loadInputSchema({commit}, options) {
|
||||
return this.$http.get(`${apiUrl(this)}/plugins/inputs/${options.type}`, {}).then(response => {
|
||||
commit("setInputSchema", response.data)
|
||||
commit("setInputSchema", response.data);
|
||||
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
loadSchemaType(_, options = {type: "flow"}) {
|
||||
return this.$http.get(`${apiUrlWithoutTenants()}/plugins/schemas/${options.type}`, {}).then(response => {
|
||||
return response.data;
|
||||
})
|
||||
});
|
||||
},
|
||||
updateDocumentation({commit, dispatch, getters}, options) {
|
||||
const taskType = options.task !== undefined ? options.task : YamlUtils.getTaskType(
|
||||
@@ -129,9 +153,9 @@ export default {
|
||||
|
||||
const taskVersion = options.event
|
||||
? YamlUtils.getVersionAtPosition(
|
||||
options?.event?.model?.getValue(),
|
||||
options?.event?.position,
|
||||
)
|
||||
options?.event?.model?.getValue(),
|
||||
options?.event?.position
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (taskType) {
|
||||
@@ -156,37 +180,37 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
setPlugin(state, plugin) {
|
||||
state.plugin = plugin
|
||||
state.plugin = plugin;
|
||||
},
|
||||
setVersions(state, versions) {
|
||||
state.versions = versions
|
||||
state.versions = versions;
|
||||
},
|
||||
setPluginAllProps(state, pluginAllProps) {
|
||||
state.pluginAllProps = pluginAllProps
|
||||
state.pluginAllProps = pluginAllProps;
|
||||
},
|
||||
setPlugins(state, plugins) {
|
||||
state.plugins = plugins
|
||||
state.plugins = plugins;
|
||||
},
|
||||
setPluginSingleList(state, pluginSingleList) {
|
||||
state.pluginSingleList = pluginSingleList
|
||||
state.pluginSingleList = pluginSingleList;
|
||||
},
|
||||
setIcons(state, icons) {
|
||||
state.icons = icons
|
||||
state.icons = icons;
|
||||
},
|
||||
setPluginsDocumentation(state, pluginsDocumentation) {
|
||||
state.pluginsDocumentation = pluginsDocumentation
|
||||
state.pluginsDocumentation = pluginsDocumentation;
|
||||
},
|
||||
addPluginDocumentation(state, pluginDocumentation) {
|
||||
state.pluginsDocumentation = {...state.pluginsDocumentation, ...pluginDocumentation}
|
||||
state.pluginsDocumentation = {...state.pluginsDocumentation, ...pluginDocumentation};
|
||||
},
|
||||
setEditorPlugin(state, editorPlugin) {
|
||||
state.editorPlugin = editorPlugin
|
||||
state.editorPlugin = editorPlugin;
|
||||
},
|
||||
setInputsType(state, inputsType) {
|
||||
state.inputsType = inputsType
|
||||
state.inputsType = inputsType;
|
||||
},
|
||||
setInputSchema(state, schema) {
|
||||
state.inputSchema = schema
|
||||
state.inputSchema = schema;
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@@ -194,5 +218,5 @@ export default {
|
||||
getPluginsDocumentation: state => state.pluginsDocumentation,
|
||||
getIcons: state => state.icons
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -185,14 +185,10 @@ export function backgroundFromState(state, alpha = 1) {
|
||||
}
|
||||
|
||||
export function getConsistentHEXColor(theme, value) {
|
||||
// if (!value) {
|
||||
// return "#ffffff";
|
||||
// }
|
||||
|
||||
let hex;
|
||||
|
||||
hex = getSchemeValue(value, "executions");
|
||||
if (hex) {
|
||||
if (hex && hex !== "transparent") {
|
||||
return hex;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {markRaw, ref, StyleValue} from "vue";
|
||||
import {within, userEvent, expect, fireEvent, waitFor} from "@storybook/test";
|
||||
import type {Meta, StoryObj} from "@storybook/vue3";
|
||||
import {within, userEvent, expect, fireEvent, waitFor} from "storybook/test";
|
||||
import type {Meta, StoryObj} from "@storybook/vue3-vite";
|
||||
import CodeTagsIcon from "vue-material-design-icons/CodeTags.vue";
|
||||
import MouseRightClickIcon from "vue-material-design-icons/MouseRightClick.vue";
|
||||
import FileTreeOutlineIcon from "vue-material-design-icons/FileTreeOutline.vue";
|
||||
|
||||
@@ -24,7 +24,7 @@ const tabs = [
|
||||
]
|
||||
|
||||
/**
|
||||
* @type {import('@storybook/vue3').StoryObj<typeof ShowCase>}
|
||||
* @type {import('@storybook/vue3-vite').StoryObj<typeof ShowCase>}
|
||||
*/
|
||||
export const Default = {
|
||||
render: () => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {ref} from "vue";
|
||||
import BarChart from "../../../../../../src/components/dashboard/components/charts/executions/BarChart.vue";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
import {within, expect, fireEvent, waitFor} from "@storybook/test";
|
||||
import {within, expect, fireEvent, waitFor} from "storybook/test";
|
||||
|
||||
const hasNavigated = ref("");
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {markRaw} from "vue";
|
||||
import type {Meta, StoryObj} from "@storybook/vue3";
|
||||
import type {Meta, StoryObj} from "@storybook/vue3-vite";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
import Card from "../../../../../src/components/dashboard/components/Card.vue";
|
||||
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import {useStore} from "vuex";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
import Executions from "../../../../src/components/executions/Executions.vue";
|
||||
import fixtureS from "./Executions-s.fixture.json";
|
||||
import {expect, userEvent, waitFor, within} from "storybook/test";
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import {isColoredAsError} from "../../utils/monacoUtils.js";
|
||||
|
||||
function getDecorators(executionsSearchData) {
|
||||
return [
|
||||
() => {
|
||||
return {
|
||||
setup() {
|
||||
const store = useStore();
|
||||
store.commit("auth/setUser", {
|
||||
id: "123",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john.doe@example.com",
|
||||
isAllowed: () => true,
|
||||
hasAnyActionOnAnyNamespace: () => true,
|
||||
});
|
||||
store.commit("misc/setConfigs", {
|
||||
hiddenLabelsPrefixes: ["system_"],
|
||||
});
|
||||
store.$http = {
|
||||
get: async (uri, _params) => {
|
||||
if (uri.endsWith("executions/search")) {
|
||||
console.log("uri", uri);
|
||||
// query params are available here if we want to make tests with them
|
||||
// console.log("params", params);
|
||||
return Promise.resolve({
|
||||
data: executionsSearchData,
|
||||
});
|
||||
}
|
||||
return Promise.resolve({data: []});
|
||||
},
|
||||
post: async (uri) => {
|
||||
console.log("post request", uri);
|
||||
|
||||
if (uri.includes("/dashboards/charts/preview")) {
|
||||
return Promise.resolve({}); // empty chart
|
||||
}
|
||||
throw new Error(
|
||||
"Unhandled fixture Request POST: " + uri,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
template: "<div style='margin:2rem'><story /></div>",
|
||||
};
|
||||
},
|
||||
vueRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: {template: "<div>home</div>"},
|
||||
},
|
||||
{
|
||||
path: "/flows/update/:namespace/:id?/:flowId?",
|
||||
name: "flows/update",
|
||||
component: {template: "<div>updateflows</div>"},
|
||||
},
|
||||
{
|
||||
path: "/executions/update/:namespace/:id?/:flowId?",
|
||||
name: "executions/update",
|
||||
component: {template: "<div>executions</div>"},
|
||||
},
|
||||
{
|
||||
path: "/executions/:id?/:flowId?",
|
||||
name: "executions/list",
|
||||
component: {template: "<div>executions</div>"},
|
||||
},
|
||||
],
|
||||
{
|
||||
initialRoute: "/executions/123/645",
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// Story configuration
|
||||
export default {
|
||||
title: "Components/Executions",
|
||||
component: Executions,
|
||||
};
|
||||
|
||||
// Stories
|
||||
export const FilterExecutions = {
|
||||
decorators: getDecorators(fixtureS),
|
||||
args: {
|
||||
hidden: [],
|
||||
statuses: [],
|
||||
isReadOnly: false,
|
||||
topbar: false,
|
||||
filter: true,
|
||||
},
|
||||
};
|
||||
|
||||
FilterExecutions.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await step("filter should contains \"timeRange\" by default", async () => {
|
||||
await waitFor(() => expect(getMonacoFilter(canvas)).toHaveTextContent("timeRange="), {timeout: 5000});
|
||||
});
|
||||
|
||||
await step(
|
||||
"clearing and adding a namespace filter with keyboard",
|
||||
async () => {
|
||||
await user.click(getMonacoFilterInput(canvas));
|
||||
await clearMonacoInput();
|
||||
await userEvent.keyboard("namespace=io.kestra");
|
||||
await refreshMonacoFilter(canvas);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent(
|
||||
"namespace=io.kestra",
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await step("adding an additional flowId filter with keyboard", async () => {
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent(
|
||||
"namespace=io.kestra",
|
||||
),
|
||||
);
|
||||
|
||||
await user.click(getMonacoFilterInput(canvas));
|
||||
await userEvent.keyboard("{End}");
|
||||
await userEvent.keyboard(spaceBarKey);
|
||||
await userEvent.keyboard("flowId=123");
|
||||
await refreshMonacoFilter(canvas);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent(
|
||||
"namespace=io.kestra",
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent("flowId=123"),
|
||||
);
|
||||
});
|
||||
|
||||
await step("unknown field should be displayed red", async () => {
|
||||
await user.click(getMonacoFilterInput(canvas));
|
||||
await clearMonacoInput();
|
||||
await userEvent.keyboard("an-unknown-field=q2132");
|
||||
await refreshMonacoFilter(canvas);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent(
|
||||
"an-unknown-field=q2132",
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
isColoredAsError(within(getMonacoFilter(canvas)).getByText(
|
||||
"an-unknown-field=q2132",
|
||||
))
|
||||
).toBeTruthy(),
|
||||
);
|
||||
});
|
||||
|
||||
await step(
|
||||
"unknown field should be marked as invalid internally by Monaco",
|
||||
async () => {
|
||||
await user.click(getMonacoFilterInput(canvas));
|
||||
await clearMonacoInput();
|
||||
await userEvent.keyboard("an-unknown-field=q2222222222");
|
||||
await refreshMonacoFilter(canvas);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getMonacoFilter(canvas)).toHaveTextContent(
|
||||
"an-unknown-field=q2222222222",
|
||||
),
|
||||
);
|
||||
const model = monaco.editor.getModels()[0];
|
||||
const tokens = monaco.editor.tokenize(
|
||||
model.getValue(),
|
||||
"executions-filter",
|
||||
);
|
||||
await expect(tokens).toBeDefined();
|
||||
await expect(tokens[0][0].type).toContain("executions-filter");
|
||||
await expect(tokens[0][0].type).toContain("invalid");
|
||||
},
|
||||
);
|
||||
};
|
||||
// TODO test from query route !!!!
|
||||
|
||||
// Helpers and constants
|
||||
const spaceBarKey = "{ }";
|
||||
const triggerRefreshButton = "trigger-refresh-button";
|
||||
const monacoFilter = "monaco-filter";
|
||||
|
||||
function getMonacoFilter(canvas) {
|
||||
return canvas.getByTestId(monacoFilter);
|
||||
}
|
||||
|
||||
function getMonacoFilterInput(canvas) {
|
||||
const monacoFilter = getMonacoFilter(canvas);
|
||||
return within(monacoFilter).getByRole("textbox");
|
||||
}
|
||||
|
||||
async function clearMonacoInput() {
|
||||
await userEvent.keyboard("{Control>}a{/Control}{Delete}");
|
||||
}
|
||||
|
||||
async function refreshMonacoFilter(canvas) {
|
||||
await userEvent.click(canvas.getByTestId(triggerRefreshButton));
|
||||
}
|
||||
442
ui/tests/storybook/components/filter/KestraFilter.stories.tsx
Normal file
442
ui/tests/storybook/components/filter/KestraFilter.stories.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
import {expect, userEvent, waitFor, within} from "storybook/test";
|
||||
import KestraFilter from "../../../../src/components/filter/KestraFilter.vue";
|
||||
import {type LocationQuery, stringifyQuery, useRoute} from "vue-router";
|
||||
import {Meta, StoryObj} from "@storybook/vue3-vite";
|
||||
import {FilterLanguage} from "../../../../src/composables/monaco/languages/filters/filterLanguage.ts";
|
||||
import {
|
||||
Comparators,
|
||||
Completion,
|
||||
FilterKeyCompletions
|
||||
} from "../../../../src/composables/monaco/languages/filters/filterCompletion.ts";
|
||||
import loadFilterLanguages from "../../mocks/services/filterLanguagesProvider.mock.ts";
|
||||
import DefaultFilterLanguage from "../../../../src/composables/monaco/languages/filters/impl/defaultFilterLanguage.ts";
|
||||
import {isColoredAsError} from "../../utils/monacoUtils.ts";
|
||||
|
||||
const meta = {
|
||||
title: "Components/KestraFilter",
|
||||
component: KestraFilter
|
||||
} satisfies Meta<typeof KestraFilter>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// Hack from Monaco editor to allow navigating through suggestions
|
||||
acceptSuggestion: () => void;
|
||||
nextSuggestion: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
let suggestionWidgetController: {
|
||||
accept: () => void,
|
||||
next: () => void
|
||||
} = {
|
||||
accept() {
|
||||
},
|
||||
next() {
|
||||
}
|
||||
};
|
||||
|
||||
function getDecorators(routeQuery?: LocationQuery) {
|
||||
return [
|
||||
() => {
|
||||
return {
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
|
||||
loadFilterLanguages.mockReturnValue(Promise.resolve([TestFilterLanguage.INSTANCE, DefaultFilterLanguage]));
|
||||
|
||||
return {route};
|
||||
},
|
||||
template: "<div><span>ROUTE QUERY: </span><span data-testid='routeQuery'>{{route.query}}</span><story /></div>",
|
||||
};
|
||||
},
|
||||
vueRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: {template: "<div>home</div>"},
|
||||
}
|
||||
],
|
||||
{
|
||||
initialRoute: "/" + (routeQuery === undefined ? "" : `?${stringifyQuery(routeQuery!)}`),
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
async function parseRouteQuery(canvas: any): Promise<LocationQuery> {
|
||||
return JSON.parse(canvas.getByTestId("routeQuery").textContent);
|
||||
}
|
||||
|
||||
// Stories
|
||||
export const KestraFilterDefault: Story = {
|
||||
decorators: getDecorators()
|
||||
};
|
||||
|
||||
KestraFilterDefault.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await step("filter is empty with default placeholder", async () => {
|
||||
await expect(await getMonacoFilterInput(canvas)).toBeEmptyDOMElement();
|
||||
await expect(getMonacoFilter(canvas).querySelector("[widgetid=\"editor.widget.placeholderHint\"]"))
|
||||
.toHaveTextContent(/^Choose filters$/)
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await step(
|
||||
"autocompletion pops upon clicking and show only text because no language is set",
|
||||
async () => {
|
||||
await waitFor(async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await assertSuggestions(canvas, ["text"]);
|
||||
}, {timeout: 5000});
|
||||
},
|
||||
);
|
||||
|
||||
await step(
|
||||
"autocompletion pops upon clicking and show only text because no language is set",
|
||||
async () => {
|
||||
await waitFor(async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await assertSuggestions(canvas, ["text"]);
|
||||
}, {timeout: 5000});
|
||||
},
|
||||
);
|
||||
|
||||
await step(
|
||||
"add some text in the filter",
|
||||
async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await userEvent.keyboard("test");
|
||||
await expect(await assertMonacoFilterContentToBe(canvas, "test"));
|
||||
|
||||
await assertRouteQuery(canvas, {"filters[q][EQUALS]": "test"});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const KestraFilterPlaceholder: Story = {
|
||||
decorators: getDecorators(),
|
||||
args: {
|
||||
placeholder: "Hello Filter"
|
||||
}
|
||||
};
|
||||
|
||||
KestraFilterPlaceholder.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await step("placeholder should be 'Hello Filter'", async () => {
|
||||
await expect(await getMonacoFilterInput(canvas)).toBeEmptyDOMElement();
|
||||
await expect(getMonacoFilter(canvas).querySelector("[widgetid=\"editor.widget.placeholderHint\"]"))
|
||||
.toHaveTextContent(new RegExp(`^${KestraFilterPlaceholder.args!.placeholder}$`));
|
||||
});
|
||||
};
|
||||
|
||||
export const KestraFilterLegacyQuery: Story = {
|
||||
decorators: getDecorators(),
|
||||
args: {
|
||||
legacyQuery: true
|
||||
}
|
||||
};
|
||||
|
||||
async function assertRouteQuery(canvas: ReturnType<typeof within>, expectedQuery: LocationQuery = {}) {
|
||||
await waitFor(async () => {
|
||||
const routeQuery = await parseRouteQuery(canvas);
|
||||
return expect(routeQuery).toStrictEqual(expectedQuery)
|
||||
}, {timeout: 5000});
|
||||
}
|
||||
|
||||
KestraFilterLegacyQuery.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await step(
|
||||
"add some text in the filter",
|
||||
async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await userEvent.keyboard("test");
|
||||
await expect(await assertMonacoFilterContentToBe(canvas, "test"));
|
||||
await assertRouteQuery(canvas, {q: "test"});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
class TestFilterLanguage extends FilterLanguage {
|
||||
static readonly FILTER_KEYS = {
|
||||
singleValue: new FilterKeyCompletions(
|
||||
[Comparators.EQUALS, Comparators.NOT_EQUALS, Comparators.STARTS_WITH],
|
||||
async () => [
|
||||
new Completion("First value", "value1"),
|
||||
new Completion("Second value", "value2")
|
||||
]
|
||||
),
|
||||
multiValue: new FilterKeyCompletions(
|
||||
[Comparators.NOT_EQUALS, Comparators.EQUALS, Comparators.STARTS_WITH],
|
||||
async () => [
|
||||
new Completion("Another first value", "anotherValue1"),
|
||||
new Completion("Another second value", "anotherValue2")
|
||||
],
|
||||
true
|
||||
),
|
||||
"nested.{key}": new FilterKeyCompletions(
|
||||
[Comparators.EQUALS]
|
||||
)
|
||||
};
|
||||
static readonly INSTANCE = new TestFilterLanguage();
|
||||
|
||||
private constructor() {
|
||||
super("test", TestFilterLanguage.FILTER_KEYS);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertSuggestions(canvas: ReturnType<typeof within>, expectedSuggestions: string[]) {
|
||||
const suggestWidget = getMonacoFilter(canvas).querySelector(".suggest-widget");
|
||||
if (suggestWidget === null) {
|
||||
throw new Error("Waiting for suggest widget to be shown");
|
||||
}
|
||||
|
||||
await expect(suggestWidget).toBeVisible();
|
||||
|
||||
const suggestions = [...getMonacoFilter(canvas).querySelectorAll(".monaco-list-row")].map(({textContent}) => textContent);
|
||||
await expect(suggestions).toEqual(expectedSuggestions);
|
||||
}
|
||||
|
||||
export const KestraFilterWithLanguage: Story = {
|
||||
decorators: getDecorators(),
|
||||
args: {
|
||||
language: TestFilterLanguage.INSTANCE as FilterLanguage
|
||||
}
|
||||
};
|
||||
|
||||
KestraFilterWithLanguage.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await step(
|
||||
"autocompletion pops upon clicking and show available keys",
|
||||
async () => {
|
||||
await waitFor(async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await assertSuggestions(canvas, [...Object.keys(TestFilterLanguage.FILTER_KEYS), "text"]);
|
||||
}, {timeout: 5000});
|
||||
},
|
||||
);
|
||||
|
||||
suggestionWidgetController = {
|
||||
accept: window.acceptSuggestion,
|
||||
next: window.nextSuggestion
|
||||
}
|
||||
|
||||
await step(
|
||||
"accepting suggestion should insert the key followed by the first comparator in the filter and proceed to value completion",
|
||||
async () => {
|
||||
let highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^singleValue$/);
|
||||
suggestionWidgetController.accept();
|
||||
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "singleValue=")));
|
||||
await assertRouteQuery(canvas, {});
|
||||
|
||||
await waitFor(async () => {
|
||||
await assertSuggestions(canvas, ["First value", "Second value"]);
|
||||
}, {timeout: 5000});
|
||||
highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^First value$/)
|
||||
|
||||
suggestionWidgetController.next();
|
||||
highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^Second value$/);
|
||||
suggestionWidgetController.accept();
|
||||
|
||||
await assertRouteQuery(canvas, {"filters[singleValue][EQUALS]": "value2"});
|
||||
|
||||
// Back to the initial suggestions as a space is automatically added after the value
|
||||
await expect(await assertMonacoFilterContentToBe(canvas, "singleValue=value2 "));
|
||||
|
||||
await waitFor(() => assertSuggestions(canvas, [...Object.keys(TestFilterLanguage.FILTER_KEYS), "text"]), {timeout: 5000});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const KestraFilterWithLanguage_MultiValueAnotherComparator: Story = {
|
||||
decorators: getDecorators(),
|
||||
args: {
|
||||
language: TestFilterLanguage.INSTANCE as FilterLanguage
|
||||
}
|
||||
};
|
||||
|
||||
KestraFilterWithLanguage_MultiValueAnotherComparator.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await step(
|
||||
"autocompletion pops upon clicking and show available keys",
|
||||
async () => {
|
||||
await waitFor(async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await assertSuggestions(canvas, [...Object.keys(TestFilterLanguage.FILTER_KEYS), "text"]);
|
||||
}, {timeout: 5000});
|
||||
},
|
||||
);
|
||||
|
||||
suggestionWidgetController = {
|
||||
accept: window.acceptSuggestion,
|
||||
next: window.nextSuggestion
|
||||
}
|
||||
|
||||
await step(
|
||||
"accepting suggestion should insert the key followed by the first comparator in the filter and proceed to value completion",
|
||||
async () => {
|
||||
suggestionWidgetController.next();
|
||||
let highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^multiValue$/);
|
||||
suggestionWidgetController.accept();
|
||||
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "multiValue!=")));
|
||||
await assertRouteQuery(canvas, {});
|
||||
|
||||
await waitFor(async () => {
|
||||
await assertSuggestions(canvas, ["Another first value", "Another second value"]);
|
||||
}, {timeout: 5000});
|
||||
highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^Another first value$/)
|
||||
|
||||
suggestionWidgetController.accept();
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "multiValue!=anotherValue1,")));
|
||||
await assertRouteQuery(canvas, {"filters[multiValue][NOT_EQUALS]": "anotherValue1"});
|
||||
|
||||
highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^Another second value$/);
|
||||
suggestionWidgetController.accept();
|
||||
|
||||
// No more suggestions as all the values are taken so we add a space
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "multiValue!=anotherValue1,anotherValue2 ")));
|
||||
// Back to the initial suggestions
|
||||
|
||||
await waitFor(() => assertSuggestions(canvas, [...Object.keys(TestFilterLanguage.FILTER_KEYS), "text"]), {timeout: 5000});
|
||||
|
||||
await assertRouteQuery(canvas, {"filters[multiValue][NOT_IN]": "anotherValue1,anotherValue2"});
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const KestraFilterWithLanguage_PopulateValueFromQuery: Story = {
|
||||
name: "Keys from query that are not compliant with language should not be added to filter",
|
||||
decorators: getDecorators({
|
||||
"filters[unknownKey][EQUALS]": "whatever",
|
||||
"filters[singleValue][EQUALS]": "unknownValue StillShouldBeAdded",
|
||||
}),
|
||||
args: {
|
||||
language: TestFilterLanguage.INSTANCE as FilterLanguage
|
||||
}
|
||||
};
|
||||
|
||||
KestraFilterWithLanguage_PopulateValueFromQuery.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await step(
|
||||
"value should be populated from query at initialization",
|
||||
async () => {
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "singleValue=\"unknownValue StillShouldBeAdded\" ")))
|
||||
// verify we kept the unknown value in the query parameters even though we didn't add it to the filter
|
||||
await new Promise(resolve => {
|
||||
setInterval(resolve, 1100);
|
||||
});
|
||||
|
||||
assertRouteQuery(canvas, {
|
||||
"filters[unknownKey][EQUALS]": "whatever",
|
||||
"filters[singleValue][EQUALS]": "unknownValue StillShouldBeAdded"
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const KestraFilterWithLanguage_NestedKey: Story = {
|
||||
decorators: getDecorators(),
|
||||
args: {
|
||||
language: TestFilterLanguage.INSTANCE as FilterLanguage
|
||||
}
|
||||
};
|
||||
|
||||
KestraFilterWithLanguage_NestedKey.play = async ({canvasElement, step}) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await step(
|
||||
"nested key autocompletion should output `nested.`",
|
||||
async () => {
|
||||
await waitFor(async () => {
|
||||
await user.click(await getMonacoFilterInput(canvas));
|
||||
await assertSuggestions(canvas, [...Object.keys(TestFilterLanguage.FILTER_KEYS), "text"]);
|
||||
}, {timeout: 5000});
|
||||
|
||||
suggestionWidgetController = {
|
||||
accept: window.acceptSuggestion,
|
||||
next: window.nextSuggestion
|
||||
}
|
||||
suggestionWidgetController.next();
|
||||
suggestionWidgetController.next();
|
||||
const highlightedSuggest = getMonacoFilter(canvas).querySelector(".monaco-list-row.focused");
|
||||
await expect(highlightedSuggest).toHaveTextContent(/^nested\.\{key}$/);
|
||||
suggestionWidgetController.accept();
|
||||
|
||||
await waitFor(async () => expect(await assertMonacoFilterContentToBe(canvas, "nested.")));
|
||||
await assertRouteQuery(canvas, {});
|
||||
}
|
||||
);
|
||||
|
||||
await step(
|
||||
"adding a nested key with a value should add not be colored as error and add the nested key as an extra [...] in the query",
|
||||
async () => {
|
||||
await userEvent.keyboard("deep.key=\"[[And Value],[[With Spaces]\"");
|
||||
|
||||
await waitFor(() => expect(
|
||||
([...getMonacoFilter(canvas).querySelectorAll(".view-lines .view-line span")] as HTMLElement[])
|
||||
.map(el => isColoredAsError(el))
|
||||
).toSatisfy<boolean[]>(areErrors => areErrors.every(isError => !isError)), {timeout: 5000});
|
||||
await assertRouteQuery(canvas, {
|
||||
"filters[nested][EQUALS][deep.key]": "[And Value],[With Spaces]"
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// NOTE We can see a bug here as we have no way to distinguish between multiple values and a single value with a comma (because allowed when having quoted value) as of now
|
||||
await step(
|
||||
"adding a comma and another value should switch the comparator to IN and add the value to the query",
|
||||
async () => {
|
||||
await userEvent.keyboard(",anotherValue");
|
||||
|
||||
await waitFor(() => expect(
|
||||
([...getMonacoFilter(canvas).querySelectorAll(".view-lines .view-line span")] as HTMLElement[])
|
||||
.map(el => isColoredAsError(el))
|
||||
).toSatisfy<boolean[]>(areErrors => areErrors.every(isError => !isError)), {timeout: 5000});
|
||||
await assertRouteQuery(canvas, {
|
||||
"filters[nested][IN][deep.key]": "[And Value],[With Spaces],anotherValue"
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const monacoFilter = "monaco-filter";
|
||||
|
||||
function getMonacoFilter(canvas: ReturnType<typeof within>) {
|
||||
return canvas.getByTestId(monacoFilter);
|
||||
}
|
||||
|
||||
async function assertMonacoFilterContentToBe(canvas: ReturnType<typeof within>, expectedText: string): Promise<void> {
|
||||
// We need to replace non-breaking spaces with regular spaces because Monaco editor uses non-breaking spaces
|
||||
await waitFor(() => expect(getMonacoFilter(canvas)).toHaveTextContent(expectedText, {normalizeWhitespace: true}));
|
||||
}
|
||||
|
||||
function getMonacoFilterInput(canvas: ReturnType<typeof within>): Promise<HTMLElement> {
|
||||
return waitFor(() => within(getMonacoFilter(canvas)).getByRole("textbox"));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {defineComponent, ref} from "vue";
|
||||
import {expect, userEvent, waitFor, within} from "@storybook/test";
|
||||
import {expect, userEvent, waitFor, within} from "storybook/test";
|
||||
import InputsForm from "../../../../src/components/inputs/InputsForm.vue";
|
||||
|
||||
const meta = {
|
||||
@@ -26,7 +26,7 @@ const Sut = defineComponent((props) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import("@storybook/vue3").StoryObj<typeof InputsForm>}
|
||||
* @type {import("@storybook/vue3-vite").StoryObj<typeof InputsForm>}
|
||||
*/
|
||||
export const InputTypes = {
|
||||
async play({canvasElement}) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
within,
|
||||
expect,
|
||||
waitFor
|
||||
} from "@storybook/test";
|
||||
} from "storybook/test";
|
||||
import LogLine from "../../../../src/components/logs/LogLine.vue";
|
||||
import {ElCard} from "element-plus";
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import loadFilterLanguages from "../../../../src/override/services/filterLanguagesProvider.ts";
|
||||
import {fn} from "storybook/test";
|
||||
|
||||
export default fn(loadFilterLanguages).mockName("loadFilterLanguages");
|
||||
@@ -8,7 +8,7 @@ const meta = {
|
||||
export default meta;
|
||||
|
||||
/**
|
||||
* @type {import('@storybook/vue3').StoryObj<typeof ShowCase>}
|
||||
* @type {import('@storybook/vue3-vite').StoryObj<typeof ShowCase>}
|
||||
*/
|
||||
export const ElementPlusPlayground = {
|
||||
render: () => <ShowCase />,
|
||||
|
||||
5
ui/tests/storybook/utils/monacoUtils.ts
Normal file
5
ui/tests/storybook/utils/monacoUtils.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function isColoredAsError(element: HTMLElement): boolean {
|
||||
const rgb = window.getComputedStyle(element)!.color!.match(/\d+/g)!.map(Number);
|
||||
// Assert red is dominant (e.g., red > 200, green & blue < 100)
|
||||
return rgb[0] > 150 && rgb[1] < 60 && rgb[2] < 60;
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "ES2021.String"],
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
"types": ["vitest/globals"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
@@ -26,7 +27,7 @@
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"override/*": ["src/override/*"]
|
||||
},
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
".storybook/preview.*",
|
||||
|
||||
@@ -36,7 +36,7 @@ export default defineConfig({
|
||||
output: {
|
||||
manualChunks
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -47,6 +47,7 @@ export default defineConfig({
|
||||
"#mdc-configs": path.resolve(__dirname, "node_modules/@kestra-io/ui-libs/stub-mdc-imports.js"),
|
||||
"shiki": path.resolve(__dirname, "node_modules/shiki/dist"),
|
||||
"vuex": path.resolve(__dirname, "node_modules/vuex/dist/vuex.esm-bundler.js"),
|
||||
"@storybook/addon-actions": "storybook/actions",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
@@ -9,7 +9,15 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"override/services/filterLanguagesProvider": path.resolve(__dirname, "tests/storybook/mocks/services/filterLanguagesProvider.mock.ts"),
|
||||
"override": path.resolve(__dirname, "src/override/"),
|
||||
"#imports": path.resolve(__dirname, "node_modules/@kestra-io/ui-libs/stub-mdc-imports.js"),
|
||||
"#build/mdc-image-component.mjs": path.resolve(__dirname, "node_modules/@kestra-io/ui-libs/stub-mdc-imports.js"),
|
||||
"#mdc-imports": path.resolve(__dirname, "node_modules/@kestra-io/ui-libs/stub-mdc-imports.js"),
|
||||
"#mdc-configs": path.resolve(__dirname, "node_modules/@kestra-io/ui-libs/stub-mdc-imports.js"),
|
||||
"shiki": path.resolve(__dirname, "node_modules/shiki/dist"),
|
||||
"vuex": path.resolve(__dirname, "node_modules/vuex/dist/vuex.esm-bundler.js"),
|
||||
"@storybook/addon-actions": "storybook/actions"
|
||||
},
|
||||
},
|
||||
test: {
|
||||
@@ -33,9 +41,10 @@ export default defineConfig({
|
||||
"**/*.stories.*",
|
||||
"**/*.d.ts",
|
||||
]
|
||||
}
|
||||
},
|
||||
projects: [".storybook/vitest.config.js"]
|
||||
},
|
||||
define: {
|
||||
"window.KESTRA_BASE_PATH": "/ui/",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import path from "node:path";
|
||||
|
||||
import {defineWorkspace} from "vitest/config";
|
||||
|
||||
import {storybookTest} from "@storybook/experimental-addon-test/vitest-plugin";
|
||||
|
||||
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
|
||||
export default defineWorkspace([
|
||||
"vitest.config.js",
|
||||
{
|
||||
extends: "vite.config.js",
|
||||
plugins: [
|
||||
// The plugin will run tests for the stories defined in your Storybook config
|
||||
// See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest
|
||||
storybookTest({configDir: path.join(__dirname, ".storybook")}),
|
||||
],
|
||||
test: {
|
||||
name: "storybook",
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: "playwright",
|
||||
instances: [{browser: "chromium"}],
|
||||
},
|
||||
setupFiles: [".storybook/vitest.setup.ts"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -125,7 +125,8 @@ public class PluginController {
|
||||
plugin.getTaskRunners().stream(),
|
||||
plugin.getLogExporters().stream(),
|
||||
plugin.getApps().stream(),
|
||||
plugin.getAppBlocks().stream()
|
||||
plugin.getAppBlocks().stream(),
|
||||
plugin.getAdditionalPlugins().stream()
|
||||
)
|
||||
.flatMap(i -> i)
|
||||
.map(e -> new AbstractMap.SimpleEntry<>(
|
||||
|
||||
Reference in New Issue
Block a user