feat(ui): refactor the schedule ui

This commit is contained in:
tchiotludo
2020-06-25 12:29:17 +02:00
committed by Ludovic DEHON
parent 8f88d024cb
commit 8a2ffba2d1
12 changed files with 350 additions and 200 deletions

View File

@@ -1,5 +1,5 @@
{
"esversion": 6,
"esversion": 9,
"asi": true,
"curly": true,
"eqeqeq": true,

View File

@@ -6,7 +6,8 @@
</div>
</template>
<span>{{content.message || content}}</span>
<b-table v-if="items && items.length > 0" striped hover :items="items"></b-table>
<b-table class="mt-2 mb-0" small bordered v-if="items && items.length > 0" striped hover
:items="items"></b-table>
</b-toast>
</template>
<script>
@@ -51,3 +52,9 @@ export default {
}
};
</script>
<style lang="scss">
@import "../styles/variable";
table {
background-color: $white;
}
</style>

View File

@@ -17,8 +17,7 @@
</template>
</b-table>
<div v-if="execution.inputs">
<hr />
<h3>{{$t('inputs')}}</h3>
<h5>{{$t('inputs')}}</h5>
<b-table
responsive="xl"
striped
@@ -41,6 +40,23 @@
</template>
</b-table>
</div>
<div v-if="variables.length > 0" class="mt-4">
<h5>{{$t('variables')}}</h5>
<b-table
responsive="xl"
striped
hover
bordered
:items="this.variables"
:fields="fields"
class="mb-0"
>
<template v-slot:cell(key)="row">
<code>{{row.item.key}}</code>
</template>
</b-table>
</div>
</div>
</template>
<script>
@@ -63,6 +79,14 @@ export default {
},
restart() {
this.$emit("follow");
},
flat(object) {
return Object.assign({}, ...function _flatten(child, path = []) {
return [].concat(...Object.keys(child).map(key => typeof child[key] === 'object'
? _flatten(child[key], path.concat([key]))
: ({ [path.concat([key]).join(".")] : child[key] })
));
}(object));
}
},
watch: {
@@ -133,9 +157,24 @@ export default {
for (const key in this.execution.inputs) {
inputs.push({ key, value: this.execution.inputs[key] });
}
console.log(inputs);
return inputs;
},
variables() {
const variables = [];
if (this.execution.variables !== undefined) {
const flat = this.flat(this.execution.variables);
for (const key in flat) {
variables.push({ key, value: flat[key] });
}
}
return variables;
}
}
},
};
</script>
<style scoped lang="scss">

View File

@@ -42,6 +42,7 @@ import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext";
import permission from "../../models/permission";
import action from "../../models/action";
import { canSaveFlow, saveFlow } from "../../utils/flow";
export default {
mixins: [RouteContext],
@@ -72,13 +73,7 @@ export default {
);
},
canSave() {
return (
this.isEdit && this.user &&
this.user.isAllowed(permission.FLOW, action.UPDATE, this.content.namespace)
) || (
!this.isEdit && this.user &&
this.user.isAllowed(permission.FLOW, action.CREATE, this.content.namespace)
);
return canSaveFlow(true, this.user, this.content);
},
canDelete() {
return this.isEdit && this.user &&
@@ -157,13 +152,8 @@ export default {
}
}
}
this.$store
.dispatch("flow/saveFlow", {
flow
})
.then(() => {
this.$toast().success({message: this.$t("flow update ok")});
})
saveFlow(this, flow)
.finally(() => {
this.loadFlow();
});

View File

@@ -110,7 +110,14 @@ export default {
});
}
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.UPDATE, this.flow.namespace)) {
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace)) {
tabs.push({
tab: "execution-configuration",
title: title("trigger")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.UPDATE, this.flow.namespace)) {
tabs.push({
tab: "data-source",
title: title("source"),
@@ -118,11 +125,12 @@ export default {
});
tabs.push({
tab: "execution-configuration",
title: title("trigger")
tab: "schedule",
title: title("schedule"),
});
}
return tabs;
}
},

View File

@@ -1,94 +1,73 @@
<template>
<div v-if="flow">
<b-row>
<b-col md="8">
<b-list-group>
<schedule-item
@remove="remove"
:schedule="schedule"
:index="x"
v-for="(schedule, x) in triggers"
:key="x"
/>
</b-list-group>
</b-col>
<b-col md="4">
<b-row>
<b-col class="text-center">
<p>
<small>Cron helper</small>
</p>
<b-table responsive :items="cronHelpData"></b-table>
<b-table responsive :items="cronHelpTokens"></b-table>
</b-col>
</b-row>
<b-row>
<b-col>
<b-form-group>
<b-btn variant="primary" @click="addSchedule">
<plus />
{{$t('add schedule') | cap}}
</b-btn>
</b-form-group>
</b-col>
</b-row>
</b-col>
</b-row>
<div>
<b-list-group>
<schedule-item
@remove="remove"
@set="set"
:schedule="schedule"
:index="x"
v-for="(schedule, x) in (flow.triggers || []) "
:key="x"
/>
</b-list-group>
<bottom-line v-if="canSave">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<b-button variant="primary" @click="addSchedule" v-if="canSave">
<plus />
{{ $t('add schedule') }}
</b-button>
<b-button @click="save" v-if="canSave">
<content-save />
<span>{{$t('save')}}</span>
</b-button>
</li>
</ul>
</bottom-line>
</div>
</template>
<script>
import { mapState } from "vuex";
import ContentSave from "vue-material-design-icons/ContentSave";
import Plus from "vue-material-design-icons/Plus";
import ScheduleItem from "./ScheduleItem";
import BottomLine from "../layout/BottomLine";
import { canSaveFlow, saveFlow } from "../../utils/flow";
export default {
components: {
Plus,
ScheduleItem
},
watch: {
flow() {
console.log("on flow change");
}
ContentSave,
ScheduleItem,
BottomLine
},
computed: {
...mapState("flow", ["flow", "triggers"]),
validForm() {
return true;
},
cronHelpData() {
const helpRecord = {};
helpRecord[this.$t("minute")] = "*";
helpRecord[this.$t("hour")] = "*";
helpRecord[this.$t("day (month)")] = "*";
helpRecord[this.$t("month")] = "*";
helpRecord[this.$t("day (week)")] = "*";
return [helpRecord];
},
cronHelpTokens() {
const helpRecord = {};
helpRecord[this.$t("any value")] = "*";
helpRecord[this.$t("value list separator")] = ",";
helpRecord[this.$t("range of values")] = "-";
helpRecord[this.$t("step values")] = "/";
return [helpRecord];
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
canSave() {
return canSaveFlow(true, this.user, this.flow);
}
},
methods: {
save() {
saveFlow(this, this.flow);
},
set(index, schedule) {
this.$store.commit("flow/setTrigger", {index, trigger: schedule});
},
remove(index) {
this.$store.commit("flow/removeTrigger", index);
this.$store.dispatch('flow/updateFlowTrigger')
},
addSchedule() {
this.$store.commit("flow/addTrigger", {
id: "schedule",
cron: "0 4 * * 1,4",
type: "org.kestra.core.models.triggers.types.Schedule"
type: "org.kestra.core.models.triggers.types.Schedule",
});
this.$store.dispatch('flow/updateFlowTrigger')
}
}
};
</script>
<style lang="scss" scoped>
</style>
</style>

View File

@@ -1,34 +1,73 @@
<template>
<b-list-group-item>
<b-row>
<b-col md="6">
{{schedule.cron}}
<b-form-group>
<b-input type="text" v-model="schedule.cron" />
</b-form-group>
<p class="text-danger" v-if="!isValid">{{$t('invalid schedule')}}</p>
<p class="text-primary" v-else>{{cronHumanReadable}}</p>
<b-btn variant="warning" @click="remove">
<delete />Remove
</b-btn>
</b-col>
<b-col md="6" class="text-center">
<div v-if="occurences.length">
<p class="font-weight-bold">3 Next occurences</p>
<p v-for="(occurence, x) in occurences" :key="x">{{occurence | date('LLL:ss')}}</p>
</div>
</b-col>
</b-row>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('id')" :label-for="'input-id-' + index">
<b-form-input required :id="'input-id-' + index" v-model="schedule.id"></b-form-input>
</b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2"
:label-for="'input-cron-' + index"
:state="isValid">
<template v-slot:label>
{{ $t('schedules.cron.expression')}}
<b-link class="text-body" :id="'tooltip-' + index">
<help />
</b-link>
<b-tooltip :target="'tooltip-' + index" placement="bottom">
<div v-if="isValid">
<p class="font-weight-bold">3 Next occurences</p>
<span v-if="occurences.length">
<span v-for="(occurence, x) in occurences" :key="x">{{occurence | date('LLL:ss')}}<br /></span>
</span>
</div>
<span v-else>
{{ $t("schedules.cron.invalid") }}
</span>
</b-tooltip>
</template>
<b-form-input required :id="'input-cron-' + index" v-model="schedule.cron"></b-form-input>
<b-form-invalid-feedback>
Enter at least 3 letters
</b-form-invalid-feedback>
<b-form-text>{{ cronHumanReadable }}</b-form-text>
</b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('schedules.cron.backfilll')"
:label-for="'input-' + index">
<date-picker
v-model="backfillStart"
:required="false"
type="datetime"
:id="'input-' + index"
></date-picker>
</b-form-group>
<b-form-group class="mb-0 text-right">
<b-btn variant="danger" @click="remove">
<delete/>
Delete
</b-btn>
</b-form-group>
</b-list-group-item>
</template>
<script>
const cronstrue = require("cronstrue/i18n");
const cronParser = require("cron-parser");
import Delete from "vue-material-design-icons/Delete";
import Help from "vue-material-design-icons/HelpBox";
import DatePicker from "vue2-datepicker";
export default {
components: {
Delete
Delete,
Help,
DatePicker
},
props: {
schedule: {
@@ -40,7 +79,26 @@ export default {
required: true
}
},
computed: {
backfillStart: {
get: function () {
return this.schedule.backfill && this.schedule.backfill.start !== undefined ?
this.$moment(this.schedule.backfill.start).toDate() :
undefined;
},
set: function (val) {
let current = this.schedule;
if (val) {
current.backfill = {"start": this.$moment(val).format()};
} else {
delete current.backfill;
}
this.$emit("set", this.index, current);
}
},
occurences() {
const occurences = [];
if (!this.isValid) {
@@ -57,7 +115,7 @@ export default {
try {
return cronstrue.toString(this.schedule.cron, { locale });
} catch {
return "invalid cron expression";
return this.$t("schedules.cron.invalid");
}
},
isValid() {
@@ -66,17 +124,8 @@ export default {
},
methods: {
remove() {
this.$bvModal
.msgBoxConfirm(this.$t("Are you sure?"))
.then(value => {
if (value) {
this.$emit("remove", this.index);
}
});
},
onChange() {
this.schedule.cron = "";
this.$emit("remove", this.index);
}
}
};
</script>
</script>

View File

@@ -12,6 +12,15 @@
@import '~vue2-datepicker/scss/index';
.mx-input {
border-radius: $border-radius;
}
.form-group {
.mx-datepicker {
width: 100%;
}
}
@import 'styles/vue-material-custom';
@import '~c3/src/scss/main';
@@ -23,4 +32,4 @@
select {
-webkit-appearance: none;
-moz-appearance: none;
}
}

View File

@@ -6,7 +6,6 @@ export default {
flow: undefined,
total: 0,
dataTree: undefined,
triggers: []
},
actions: {
@@ -55,26 +54,60 @@ export default {
return Vue.axios.get(`/api/v1/flows/${flow.namespace}/${flow.id}/tree`).then(response => {
commit('setDataTree', response.data.tasks)
})
},
}
},
mutations: {
setFlows(state, flows) {
state.flows = flows
},
setFlow(state, flow) {
state.flow = flow
if (flow.triggers) {
state.triggers = flow.triggers
if (flow.triggers !== undefined) {
flow.triggers = flow.triggers.map(trigger => {
if (trigger.backfill === undefined) {
trigger.backfill = {
start: undefined
}
}
return trigger;
})
}
state.flow = {...flow}
},
setTriggers(state, triggers) {
state.triggers = triggers
setTrigger(state, {index, trigger}) {
let flow = state.flow;
if (flow.triggers === undefined) {
flow.triggers = []
}
flow.triggers[index] = trigger;
state.flow = {...flow}
},
removeTrigger(state, index) {
state.triggers.splice(index, 1);
let flow = state.flow;
flow.triggers.splice(index, 1);
state.flow = {...flow}
},
addTrigger(state, trigger) {
state.triggers.push(trigger)
let flow = state.flow;
if (trigger.backfill === undefined) {
trigger.backfill = {
start: undefined
}
}
if (flow.triggers === undefined) {
flow.triggers = []
}
flow.triggers.push(trigger)
state.flow = {...flow}
},
setTotal(state, total) {
state.total = total
@@ -86,9 +119,7 @@ export default {
getters: {
flow (state) {
if (state.flow) {
const flow = state.flow
flow.triggers = state.triggers
return flow
return state.flow;
}
}
}

View File

@@ -5,116 +5,116 @@
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #eee !default;
$gray-300: #dee2e6 !default;
$gray-400: #ccc !default;
$gray-500: #adb5bd !default;
$gray-600: #888 !default;
$gray-700: #495057 !default;
$gray-800: #333 !default;
$gray-900: #222 !default;
$black: #000 !default;
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #eee;
$gray-300: #dee2e6;
$gray-400: #ccc;
$gray-500: #adb5bd;
$gray-600: #888;
$gray-700: #495057;
$gray-800: #333;
$gray-900: #222;
$black: #000;
$blue: #1AA5DE !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #F04124 !default;
$orange: #fd7e14 !default;
$yellow: #FBD10B !default;
$green: #43ac6a !default;
$teal: #1DBAAF !default;
$cyan: #5bc0de !default;
$blue: #1AA5DE;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #F04124;
$orange: #fd7e14;
$yellow: #FBD10B;
$green: #43ac6a;
$teal: #1DBAAF;
$cyan: #5bc0de;
$primary: $blue !default;
$secondary: $teal !default;
$primary: $blue;
$secondary: $teal;
$tertiary: $yellow;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-200 !default;
$dark: $gray-900 !default;
$success: $green;
$info: $cyan;
$warning: $yellow;
$danger: $red;
$light: $gray-200;
$dark: $gray-900;
$yiq-contrasted-threshold: 200 !default;
$yiq-contrasted-threshold: 200;
// Components
$border-radius: 0px !default;
$border-radius-lg: 4px !default;
$border-radius-sm: 0px !default;
$border-radius: 0px;
$border-radius-lg: 4px;
$border-radius-sm: 0px;
// Fonts
$font-family-sans-serif: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$font-family-monospace: 'Source Code Pro', SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
$font-family-sans-serif: "Open Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$font-family-monospace: 'Source Code Pro', SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
$font-size-base: 1rem !default;
$font-size-xs: $font-size-base * .75 !default;
$font-size-base: 1rem;
$font-size-xs: $font-size-base * .75;
$headings-font-weight: 300 !default;
$text-muted: $gray-700 !default;
$headings-font-weight: 300;
$text-muted: $gray-700;
// Buttons
$input-btn-padding-x: 0.5rem !default;
$input-btn-padding-x: 0.5rem;
$btn-font-weight: 300 !default;
$btn-font-weight: 300;
// Dropdowns
$dropdown-border-color: rgba($black, .1) !default;
$dropdown-divider-bg: rgba($black, .1) !default;
$dropdown-border-color: rgba($black, .1);
$dropdown-divider-bg: rgba($black, .1);
// Navs
$nav-link-disabled-color: $gray-400 !default;
$nav-link-disabled-color: $gray-400;
$nav-tabs-border-color: $dropdown-border-color !default;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color !default;
$nav-tabs-link-active-border-color: $nav-tabs-border-color !default;
$nav-tabs-border-color: $dropdown-border-color;
$nav-tabs-link-hover-border-color: $nav-tabs-border-color;
$nav-tabs-link-active-border-color: $nav-tabs-border-color;
// Navbar
$navbar-dark-color: rgba($white, .7) !default;
$navbar-dark-hover-color: $white !default;
$navbar-dark-color: rgba($white, .7);
$navbar-dark-hover-color: $white;
// Pagination
$pagination-color: $gray-600 !default;
$pagination-border-color: $nav-tabs-border-color !default;
$pagination-color: $gray-600;
$pagination-border-color: $nav-tabs-border-color;
$pagination-active-border-color: darken($primary, 5%) !default;
$pagination-active-border-color: darken($primary, 5%);
// Jumbotron
$jumbotron-padding: 4rem !default;
$jumbotron-padding: 4rem;
// Cards
$card-inner-border-radius: 0px !default;
$card-inner-border-radius: 0px;
// Badges
$badge-font-weight: 300 !default;
$badge-padding-x: 1rem !default;
$badge-font-weight: 300;
$badge-padding-x: 1rem;
// Progress bars
$progress-bg: $gray-400 !default;
$progress-bar-color: $white !default;
$progress-bg: $gray-400;
$progress-bar-color: $white;
// List group
$list-group-disabled-bg: $gray-200 !default;
$list-group-disabled-bg: $gray-200;
// Close
$close-color: $gray-600 !default;
$close-text-shadow: none !default;
$close-color: $gray-600;
$close-text-shadow: none;
// Breadcrumb
$breadcrumb-item-padding: 0.25rem;
$breadcrumb-item-padding: 0.25rem;

View File

@@ -1,7 +1,7 @@
{
"en": {
"id": "Id",
"namespace": "namespace",
"namespace": "Namespace",
"revision": "Revision",
"Language": "Language",
"Set default page": "Set default page",
@@ -96,10 +96,18 @@
"down trend": "Decreasing",
"flow update aborted": "Flow update aborted",
"invalid flow": "Invalid flow",
"add schedule": "add schedule",
"schedule": "schedule",
"add schedule": "Add a schedule",
"schedule": "Schedule",
"schedules": {
"cron": {
"expression": "Expression",
"backfilll": "Backfill",
"invalid": "Invalid cron expression"
}
},
"inputs": "Inputs",
"input": "Input",
"variables": "Variables",
"download": "Download"
},
"fr": {
@@ -200,10 +208,18 @@
"down trend": "En baisse",
"flow update aborted": "Mise à jour du flow annulée",
"invalid flow": "Flow invalide",
"add schedule": "ajouter récurrence",
"schedule": "tâche programmée",
"add schedule": "Ajouter un plannificationS",
"schedule": "Tâche plannifié",
"schedules": {
"cron": {
"expression": "Expression",
"backfilll": "Remblayage",
"invalid": "cron expression invalide"
}
},
"inputs": "Entrées",
"input": "Entré",
"variables": "Variables",
"download": "Télécharger"
}
}

22
ui/src/utils/flow.js Normal file
View File

@@ -0,0 +1,22 @@
import permission from "../models/permission";
import action from "../models/action";
export function canSaveFlow(isEdit, user, flow) {
return (
isEdit && user &&
user.isAllowed(permission.FLOW, action.UPDATE, flow.namespace)
) || (
!isEdit && user &&
user.isAllowed(permission.FLOW, action.CREATE, flow.namespace)
);
}
export function saveFlow(self, flow) {
return self.$store
.dispatch("flow/saveFlow", {
flow
})
.then(() => {
self.$toast().success({message: self.$t("flow update ok")});
})
}