Skip to content

Commit

Permalink
feat(ui): introduce KV Store
Browse files Browse the repository at this point in the history
  • Loading branch information
brian-mulier-p committed Jul 4, 2024
1 parent 9c5c4ca commit 8f5dcff
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 141 deletions.
6 changes: 5 additions & 1 deletion core/src/main/java/io/kestra/core/storages/kv/KVEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@

import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

public record KVEntry(String key, Instant creationDate, Instant updateDate, Instant expirationDate) {
public static KVEntry from(FileAttributes fileAttributes) throws IOException {
return new KVEntry(
fileAttributes.getFileName().replace(".ion", ""),
Instant.ofEpochMilli(fileAttributes.getCreationTime()),
Instant.ofEpochMilli(fileAttributes.getLastModifiedTime()),
new KVMetadata(fileAttributes.getMetadata()).getExpirationDate()
Optional.ofNullable(new KVMetadata(fileAttributes.getMetadata()).getExpirationDate())
.map(expirationDate -> expirationDate.truncatedTo(ChronoUnit.MILLIS))
.orElse(null)
);
}
}
24 changes: 21 additions & 3 deletions core/src/main/java/io/kestra/core/storages/kv/KVStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
import io.kestra.core.exceptions.ResourceExpiredException;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.storages.StorageContext;
import io.kestra.core.utils.Rethrow;

import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

import static io.kestra.core.utils.Rethrow.throwFunction;

/**
* Service interface for accessing the files attached to a namespace Key-Value store.
*/
public interface KVStore {
Pattern durationPattern = Pattern.compile("^P(?=.)(?:\\d*D)?(?:T(?=.)(?:\\d*H)?(?:\\d*M)?(?:\\d*S)?)?$");

default void validateKey(String key) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Key cannot be null or empty");
Expand All @@ -38,13 +41,28 @@ default URI storageUri(String key, String namespace) {
}

default void put(String key, KVStoreValueWrapper<Object> kvStoreValueWrapper) throws IOException {
this.putRaw(key, KVStoreValueWrapper.ionStringify(kvStoreValueWrapper));
Object value = kvStoreValueWrapper.value();
String ionValue;
if (value instanceof Duration duration) {
ionValue = duration.toString();
} else {
ionValue = JacksonMapper.ofIon().writeValueAsString(value);
}

this.putRaw(key, new KVStoreValueWrapper<>(kvStoreValueWrapper.kvMetadata(), ionValue));
}

void putRaw(String key, KVStoreValueWrapper<String> kvStoreValueWrapper) throws IOException;

default Optional<Object> get(String key) throws IOException, ResourceExpiredException {
return this.getRaw(key).map(throwFunction(raw -> JacksonMapper.ofIon().readValue(raw, Object.class)));
return this.getRaw(key).map(throwFunction(raw -> {
Object value = JacksonMapper.ofIon().readValue(raw, Object.class);
if (value instanceof String valueStr && durationPattern.matcher(valueStr).matches()) {
return Duration.parse(valueStr);
}

return value;
}));
}

Optional<String> getRaw(String key) throws IOException, ResourceExpiredException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.kestra.core.storages.kv;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.kestra.core.serializers.JacksonMapper;
import io.kestra.core.storages.StorageObject;

import java.io.IOException;
Expand Down Expand Up @@ -36,8 +34,4 @@ static KVStoreValueWrapper<String> from(StorageObject storageObject) throws IOEx
return new KVStoreValueWrapper<>(new KVMetadata(storageObject.metadata()), ionString);
}
}

static KVStoreValueWrapper<String> ionStringify(KVStoreValueWrapper<Object> kvStoreValueWrapper) throws JsonProcessingException {
return new KVStoreValueWrapper<>(kvStoreValueWrapper.kvMetadata(), JacksonMapper.ofIon().writeValueAsString(kvStoreValueWrapper.value()));
}
}
4 changes: 2 additions & 2 deletions core/src/test/java/io/kestra/plugin/core/kv/SetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void defaultCase() throws Exception {
set.run(runContext);

final KVStore kv = runContext.storage().namespaceKv(namespaceId);
assertThat(kv.get(key), is(value));
assertThat(kv.get(key).get(), is(value));
assertThat(kv.list().get(0).expirationDate(), nullValue());
}

Expand All @@ -76,7 +76,7 @@ void ttl() throws Exception {
set.run(runContext);

final KVStore kv = runContext.storage().namespaceKv(namespaceId);
assertThat(kv.get(key), is(value));
assertThat(kv.get(key).get(), is(value));
Instant expirationDate = kv.list().get(0).expirationDate();
assertThat(expirationDate.isAfter(Instant.now().plus(Duration.ofMinutes(4))) && expirationDate.isBefore(Instant.now().plus(Duration.ofMinutes(6))), is(true));
}
Expand Down
6 changes: 3 additions & 3 deletions ui/src/components/executions/date-select/DateFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
:end-date="endDate"
@update:model-value="onAbsFilterChange"
/>
<relative-date-select
<time-select
v-if="selectedFilterType === filterType.RELATIVE"
:time-range="timeRange"
@update:model-value="onRelFilterChange"
Expand All @@ -26,12 +26,12 @@

<script>
import DateRange from "../../layout/DateRange.vue";
import RelativeDateSelect from "./RelativeDateSelect.vue";
import TimeSelect from "./TimeSelect.vue";
export default {
components: {
DateRange,
RelativeDateSelect
TimeSelect
},
emits: [
"update:isRelative",
Expand Down
16 changes: 13 additions & 3 deletions ui/src/components/executions/date-select/DateSelect.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<template>
<el-tooltip :content="tooltip" effect="light">
<el-tooltip :disabled="tooltip === undefined" :content="tooltip" effect="light">
<el-select
:model-value="value"
:placeholder="placeholder"
@change="$emit('change', $event)"
:clearable="clearable"
>
<template #prefix>
<clock-outline />
Expand All @@ -28,17 +30,25 @@
"change"
],
props: {
placeholder: {
type: String,
default: undefined
},
value: {
type: String,
required: true
default: undefined
},
options: {
type: Array,
default: () => []
},
tooltip: {
type: String,
required: true
default: undefined
},
clearable: {
type: Boolean,
default: false
}
}
}
Expand Down
74 changes: 0 additions & 74 deletions ui/src/components/executions/date-select/RelativeDateSelect.vue

This file was deleted.

128 changes: 128 additions & 0 deletions ui/src/components/executions/date-select/TimeSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<template>
<date-select
:placeholder="customAwarePlaceholder"
:value="timeRangeSelect"
:options="timeFilterPresets"
:tooltip="fromNow ? $t('relative start date') : undefined"
:clearable="clearable"
@change="onTimeRangeSelect"
/>
<el-tooltip v-if="allowCustom && timeRangeSelect === undefined" :content="allowInfinite ? $t('datepicker.leave empty for infinite') : $t('datepicker.duration example')">
<el-input class="mt-2" :model-value="timeRange" :placeholder="$t('datepicker.custom duration')" @update:model-value="onTimeRangeChange" />
</el-tooltip>
</template>

<script>
import DateSelect from "./DateSelect.vue";
export default {
components: {
DateSelect
},
emits: [
"update:modelValue"
],
computed: {
customAwarePlaceholder() {
if (this.placeholder) {
return this.placeholder;
}
return this.allowCustom ? this.$t("datepicker.custom") : undefined;
},
presetValues() {
return this.timeFilterPresets.map(preset => preset.value);
}
},
watch: {
timeRange: {
handler(newValue, oldValue) {
if (oldValue === undefined && this.presetValues.includes(newValue)) {
this.onTimeRangeSelect(newValue);
}
},
immediate: true
}
},
data() {
return {
timeRangeSelect: undefined,
timeFilterPresets: [
{
value: "PT5M",
label: this.label("5minutes")
},
{
value: "PT15M",
label: this.label("15minutes")
},
{
value: "PT1H",
label: this.label("1hour")
},
{
value: "PT12H",
label: this.label("12hours")
},
{
value: "PT24H",
label: this.label("24hours")
},
{
value: "PT48H",
label: this.label("48hours")
},
{
value: "PT168H",
label: this.label("7days")
},
{
value: "PT720H",
label: this.label("30days")
},
{
value: "PT8760H",
label: this.label("365days")
}
]
}
},
props: {
allowCustom: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: undefined
},
timeRange: {
type: String,
default: undefined
},
fromNow: {
type: Boolean,
default: true
},
allowInfinite: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
}
},
methods: {
onTimeRangeSelect(range) {
this.timeRangeSelect = range;
this.onTimeRangeChange(range);
},
onTimeRangeChange(range) {
this.$emit("update:modelValue", {"timeRange": range});
},
label(duration) {
return "datepicker." + (this.fromNow ? "last" : "") + duration;
}
}
}
</script>
Loading

0 comments on commit 8f5dcff

Please sign in to comment.