Skip to content

Commit

Permalink
feat(*): introduce custom dashboards (#6144)
Browse files Browse the repository at this point in the history
closes kestra-io/kestra-ee#1711

Co-authored-by: MilosPaunovic <[email protected]>
  • Loading branch information
brian-mulier-p and MilosPaunovic authored Nov 29, 2024
1 parent 8212106 commit 4943f9a
Show file tree
Hide file tree
Showing 127 changed files with 4,387 additions and 167 deletions.
47 changes: 40 additions & 7 deletions core/src/main/java/io/kestra/core/docs/JsonSchemaGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
import com.google.common.collect.ImmutableMap;
import io.kestra.core.models.property.Data;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.annotations.PluginProperty;
import io.kestra.core.models.conditions.Condition;
import io.kestra.core.models.conditions.ScheduleCondition;
import io.kestra.core.models.dashboards.DataFilter;
import io.kestra.core.models.dashboards.charts.Chart;
import io.kestra.core.models.dashboards.charts.DataChart;
import io.kestra.core.models.property.Data;
import io.kestra.core.models.property.Property;
import io.kestra.core.models.tasks.Output;
import io.kestra.core.models.tasks.Task;
import io.kestra.core.models.tasks.common.EncryptedString;
Expand All @@ -31,14 +34,12 @@
import io.kestra.core.plugins.RegisteredPlugin;
import io.kestra.core.serializers.JacksonMapper;
import io.micronaut.core.annotation.Nullable;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.*;
import java.time.Duration;
import java.time.LocalTime;
import java.util.*;
Expand Down Expand Up @@ -334,11 +335,20 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
}
});

// PluginProperty additionalProperties
builder.forFields().withAdditionalPropertiesResolver(target -> {
PluginProperty pluginPropertyAnnotation = target.getAnnotationConsideringFieldAndGetter(PluginProperty.class);
Schema schemaAnnotation = target.getAnnotationConsideringFieldAndGetter(Schema.class);
Content contentAnnotation = target.getAnnotationConsideringFieldAndGetter(Content.class);
Schema contentSchemaAnnotation = contentAnnotation == null ? null : contentAnnotation.additionalPropertiesSchema();

if (pluginPropertyAnnotation != null) {
return pluginPropertyAnnotation.additionalProperties();
} else if (target.getType().isInstanceOf(Map.class)) {
return target.getTypeParameterFor(Map.class, 1);
} else if (schemaAnnotation != null && schemaAnnotation.additionalPropertiesSchema() != Void.class) {
return schemaAnnotation.additionalPropertiesSchema();
} else if (contentSchemaAnnotation != null && contentSchemaAnnotation.additionalPropertiesSchema() != Void.class) {
return contentSchemaAnnotation.additionalPropertiesSchema();
}

return Object.class;
Expand Down Expand Up @@ -472,6 +482,29 @@ protected List<ResolvedType> subtypeResolver(ResolvedType declaredType, TypeCont
.filter(Predicate.not(io.kestra.core.models.Plugin::isInternal))
.flatMap(clz -> safelyResolveSubtype(declaredType, clz, typeContext).stream())
.toList();
} else if (declaredType.getErasedType() == Chart.class) {
return getRegisteredPlugins()
.stream()
.flatMap(registeredPlugin -> registeredPlugin.getCharts().stream())
.filter(Predicate.not(io.kestra.core.models.Plugin::isInternal))
.<ResolvedType>mapMulti((clz, consumer) -> {
if (DataChart.class.isAssignableFrom(clz)) {
List<Class<? extends DataFilter<?, ?>>> dataFilters = getRegisteredPlugins()
.stream()
.flatMap(registeredPlugin -> registeredPlugin.getDataFilters().stream())
.filter(Predicate.not(io.kestra.core.models.Plugin::isInternal))
.toList();

TypeVariable<? extends Class<? extends Chart<?>>> dataFilterType = clz.getTypeParameters()[1];
ParameterizedType chartAwareColumnDescriptor = ((ParameterizedType) ((WildcardType) ((ParameterizedType) dataFilterType.getBounds()[0]).getActualTypeArguments()[1]).getUpperBounds()[0]);
dataFilters.forEach(dataFilter -> {
Type fieldsEnum = ((ParameterizedType) dataFilter.getGenericSuperclass()).getActualTypeArguments()[0];
consumer.accept(typeContext.resolve(clz, fieldsEnum, typeContext.resolve(dataFilter, typeContext.resolve(chartAwareColumnDescriptor, fieldsEnum))));
});
} else {
consumer.accept(typeContext.resolve(clz));
}
}).toList();
}

return null;
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/io/kestra/core/docs/SchemaType.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public enum SchemaType {
task,
trigger,
plugindefault,
apps
apps,
dashboard
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.kestra.core.models.dashboards;

public enum AggregationType {
AVG,
MAX,
MIN,
SUM,
COUNT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.kestra.core.models.dashboards;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.util.Collections;
import java.util.List;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@EqualsAndHashCode
public class ChartOption {
@NotNull
@NotBlank
private String displayName;

private String description;

public List<String> neededColumns() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.kestra.core.models.dashboards;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@EqualsAndHashCode
public class ColumnDescriptor<F extends Enum<F>> {
private F field;
private String displayName;
private AggregationType agg;
private String labelKey;
}
90 changes: 90 additions & 0 deletions core/src/main/java/io/kestra/core/models/dashboards/Dashboard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.kestra.core.models.dashboards;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.kestra.core.models.DeletedInterface;
import io.kestra.core.models.HasUID;
import io.kestra.core.models.dashboards.charts.Chart;
import io.kestra.core.utils.IdUtils;
import io.micronaut.core.annotation.Introspected;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import java.time.Instant;
import java.util.List;
import java.util.Objects;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@Introspected
@ToString
public class Dashboard implements HasUID, DeletedInterface {
@Hidden
@Pattern(regexp = "^[a-z0-9][a-z0-9_-]*")
private String tenantId;

@Hidden
private String id;

@NotNull
@NotBlank
private String title;

private String description;

@Valid
@Builder.Default
private TimeWindow timeWindow = TimeWindow.builder().build();

@Valid
private List<Chart<?>> charts;

@Hidden
@NotNull
@Builder.Default
private boolean deleted = false;

@Hidden
private Instant created;

@Hidden
private Instant updated;

private String sourceCode;

@Override
@JsonIgnore
public String uid() {
return IdUtils.fromParts(
tenantId,
id
);
}

public Dashboard toDeleted() {
return this.toBuilder()
.deleted(true)
.build();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dashboard dashboard = (Dashboard) o;
return deleted == dashboard.deleted && Objects.equals(tenantId, dashboard.tenantId) && Objects.equals(id, dashboard.id) && Objects.equals(title, dashboard.title) && Objects.equals(description, dashboard.description) && Objects.equals(timeWindow, dashboard.timeWindow) && Objects.equals(charts, dashboard.charts) && Objects.equals(sourceCode, dashboard.sourceCode);
}

@Override
public int hashCode() {
return Objects.hash(tenantId, id, title, description, timeWindow, charts, deleted, sourceCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.kestra.core.models.dashboards;

import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.dashboards.filters.AbstractFilter;
import io.kestra.core.repositories.QueryBuilderInterface;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@Plugin
@EqualsAndHashCode
public abstract class DataFilter<F extends Enum<F>, C extends ColumnDescriptor<F>> implements io.kestra.core.models.Plugin {
@NotNull
@NotBlank
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
private String type;

private Map<String, C> columns;

private List<AbstractFilter<F>> where;

private List<OrderBy> orderBy;

public Set<F> aggregationForbiddenFields() {
return Collections.emptySet();
}

public abstract Class<? extends QueryBuilderInterface<F>> repositoryClass();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.kestra.core.models.dashboards;

public enum GraphStyle {
LINES,
BARS,
POINTS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.kestra.core.models.dashboards;

public enum Order {
ASC,
DESC
}
22 changes: 22 additions & 0 deletions core/src/main/java/io/kestra/core/models/dashboards/OrderBy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.kestra.core.models.dashboards;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@EqualsAndHashCode
public class OrderBy {
@NotNull
@NotBlank
private String column;

@Builder.Default
private Order order = Order.ASC;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.kestra.core.models.dashboards;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.kestra.core.validations.DashboardWindowValidation;
import io.micronaut.core.annotation.Introspected;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.time.DurationMax;

import java.time.Duration;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@Introspected
@ToString
@EqualsAndHashCode
@DashboardWindowValidation
public class TimeWindow {
@DurationMax(days = 366L, message = "Time window can't be more than 1 year (366 days).")
@JsonProperty("default")
@Builder.Default
private Duration defaultDuration = Duration.ofDays(30);

@DurationMax(days = 366L, message = "Time window can't be more than 1 year (366 days).")
@Builder.Default
private Duration max = Duration.ofDays(366);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.kestra.core.models.dashboards;

import io.kestra.core.models.dashboards.charts.LegendOption;

public interface WithLegend {
LegendOption getLegend();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.kestra.core.models.dashboards;

import io.kestra.core.models.dashboards.charts.TooltipBehaviour;

public interface WithTooltip {
TooltipBehaviour getTooltip();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.kestra.core.models.dashboards.charts;

import com.fasterxml.jackson.annotation.JsonInclude;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.dashboards.ChartOption;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@SuperBuilder(toBuilder = true)
@Getter
@NoArgsConstructor
@Plugin
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@EqualsAndHashCode
public abstract class Chart<P extends ChartOption> implements io.kestra.core.models.Plugin {
@NotNull
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9][a-zA-Z0-9_-]*")
private String id;

@NotNull
@NotBlank
@Pattern(regexp = "\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*")
protected String type;

private P chartOptions;
}
Loading

0 comments on commit 4943f9a

Please sign in to comment.