fix(ui) add a better linter configuration

This commit is contained in:
Ludovic DEHON
2020-11-04 11:52:18 +01:00
committed by tchiotludo
parent 367f951900
commit cd99681712
89 changed files with 2781 additions and 2633 deletions

25
.editorconfig Normal file
View File

@@ -0,0 +1,25 @@
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=false
trim_trailing_whitespace=true
indent_style=space
indent_size=4
continuation_indent_size=4
[*.yml]
indent_size=2
[*.md]
indent_size=2
[*.yaml]
indent_size=2
[*.json]
indent_size=2
[*.css]
indent_size=2

View File

@@ -1,5 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: ["@vue/app"]
'@vue/app' };
]
}

View File

@@ -65,10 +65,37 @@
"node": true "node": true
}, },
"extends": [ "extends": [
"plugin:vue/essential", "plugin:vue/strongly-recommended",
"eslint:recommended" "eslint:recommended"
], ],
"rules": {}, "rules": {
"vue/html-indent": [
"error",
4,
{
"baseIndent": 1
}
],
"vue/script-indent": [
"error",
4,
{
"baseIndent": 1
}
],
"vue/max-attributes-per-line": [
"error",
{
"singleline": 7
}
],
"quotes": [
"error",
"double"
],
"vue/object-curly-spacing": ["error", "never"],
"object-curly-spacing": ["error", "never"]
},
"parserOptions": { "parserOptions": {
"parser": "babel-eslint" "parser": "babel-eslint"
} }

View File

@@ -1,59 +1,59 @@
<template> <template>
<div> <div>
<nprogress-container></nprogress-container> <nprogress-container />
<top-nav-bar :menuCollapsed="menuCollapsed" /> <top-nav-bar :menu-collapsed="menuCollapsed" />
<Menu @onMenuCollapse="onMenuCollapse" /> <Menu @onMenuCollapse="onMenuCollapse" />
<custom-toast v-if="errorMessage" :noAutoHide="true" toastId="errorToast" :content="errorMessage" :title="$t('error')" /> <custom-toast v-if="errorMessage" :no-auto-hide="true" toast-id="errorToast" :content="errorMessage" :title="$t('error')" />
<div id="app" class="container-fluid"> <div id="app" class="container-fluid">
<div class="content-wrapper" :class="menuCollapsed"> <div class="content-wrapper" :class="menuCollapsed">
<router-view></router-view> <router-view />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Menu from "Override/components/Menu.vue"; import Menu from "Override/components/Menu.vue";
import TopNavBar from "./components/layout/TopNavBar"; import TopNavBar from "./components/layout/TopNavBar";
import CustomToast from "./components/customToast"; import CustomToast from "./components/customToast";
import NprogressContainer from "vue-nprogress/src/NprogressContainer"; import NprogressContainer from "vue-nprogress/src/NprogressContainer";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
name: "app", name: "App",
components: { components: {
Menu, Menu,
TopNavBar, TopNavBar,
CustomToast, CustomToast,
NprogressContainer NprogressContainer
},
data() {
return {
menuCollapsed: "",
};
},
computed: {
...mapState('core', ['errorMessage'])
},
created() {
if (this.$route.path === "/") {
this.$router.push({ name: "flowsList" });
}
this.displayApp()
this.onMenuCollapse(localStorage.getItem("menuCollapsed") === "true");
},
methods: {
onMenuCollapse(collapse) {
this.menuCollapsed = collapse ? "menu-collapsed" : "menu-not-collapsed";
}, },
displayApp() { data() {
document.getElementById("loader-wrapper").style.display = "none"; return {
document.getElementById("app-container").style.display = "block"; menuCollapsed: "",
};
},
computed: {
...mapState("core", ["errorMessage"])
},
created() {
if (this.$route.path === "/") {
this.$router.push({name: "flowsList"});
}
this.displayApp()
this.onMenuCollapse(localStorage.getItem("menuCollapsed") === "true");
},
methods: {
onMenuCollapse(collapse) {
this.menuCollapsed = collapse ? "menu-collapsed" : "menu-not-collapsed";
},
displayApp() {
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("app-container").style.display = "block";
}
} }
} };
};
</script> </script>

View File

@@ -1,45 +1,45 @@
<template> <template>
<b-button class="status text-white rounded-lg" :class="'btn-' + cls"> <b-button class="status text-white rounded-lg" :class="'btn-' + cls">
<component :is="icon"></component> <component :is="icon" />
{{status | lower | cap }} {{ status | lower | cap }}
</b-button> </b-button>
</template> </template>
<script> <script>
import State from "../utils/state"; import State from "../utils/state";
import PauseCircleOutline from "vue-material-design-icons/PauseCircleOutline"; import PauseCircleOutline from "vue-material-design-icons/PauseCircleOutline";
import CheckCircleOutline from "vue-material-design-icons/CheckCircleOutline"; import CheckCircleOutline from "vue-material-design-icons/CheckCircleOutline";
import PlayCircleOutline from "vue-material-design-icons/PlayCircleOutline"; import PlayCircleOutline from "vue-material-design-icons/PlayCircleOutline";
import CloseCircleOutline from "vue-material-design-icons/CloseCircleOutline"; import CloseCircleOutline from "vue-material-design-icons/CloseCircleOutline";
import StopCircleOutline from "vue-material-design-icons/StopCircleOutline"; import StopCircleOutline from "vue-material-design-icons/StopCircleOutline";
export default { export default {
components: { components: {
PauseCircleOutline, PauseCircleOutline,
CheckCircleOutline, CheckCircleOutline,
PlayCircleOutline, PlayCircleOutline,
CloseCircleOutline, CloseCircleOutline,
StopCircleOutline StopCircleOutline
},
props: {
status: {
type: String,
required: true
}, },
size: { props: {
type: String, status: {
default: "" type: String,
} required: true
}, },
computed: { size: {
cls() { type: String,
return State.colorClass()[this.status] + (this.size ? " btn-" + this.size : ""); default: ""
}
}, },
icon () { computed: {
return State.icon()[this.status]; cls() {
return State.colorClass()[this.status] + (this.size ? " btn-" + this.size : "");
},
icon () {
return State.icon()[this.status];
}
} }
} };
};
</script> </script>
<style scoped> <style scoped>
button.status { button.status {

View File

@@ -2,55 +2,62 @@
<b-toast @hide="onHide" :id="toastId" :variant="variant" solid :no-auto-hide="noAutoHide"> <b-toast @hide="onHide" :id="toastId" :variant="variant" solid :no-auto-hide="noAutoHide">
<template v-slot:toast-title> <template v-slot:toast-title>
<div class="d-flex flex-grow-1 align-items-baseline"> <div class="d-flex flex-grow-1 align-items-baseline">
<strong class="mr-auto">{{title}}</strong> <strong class="mr-auto">{{ title }}</strong>
</div> </div>
</template> </template>
<span>{{content.message || content}}</span> <span>{{ content.message || content }}</span>
<b-table class="mt-2 mb-0" small bordered v-if="items && items.length > 0" striped hover <b-table
:items="items"></b-table> class="mt-2 mb-0"
small
bordered
v-if="items && items.length > 0"
striped
hover
:items="items"
/>
</b-toast> </b-toast>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
variant: { variant: {
type: String, type: String,
default: "danger" default: "danger"
},
title: {
type: String,
required: true
},
toastId: {
type: String,
required: true
},
content: {
type: Object,
required: true
},
noAutoHide: {
type: Boolean,
default: false
}
}, },
title: { mounted() {
type: String, this.$bvToast.show(this.toastId);
required: true
}, },
toastId: { computed: {
type: String, items() {
required: true const messages = this.content && this.content._embedded && this.content._embedded.errors ? this.content._embedded.errors : []
return Array.isArray(messages) ? messages : [messages]
}
}, },
content: { methods: {
type: Object, onHide() {
required: true setTimeout(() => {
}, this.$store.commit("core/setErrorMessage", undefined);
noAutoHide: { }, 1000);
type: Boolean, }
default: false
} }
}, };
mounted() {
this.$bvToast.show(this.toastId);
},
computed: {
items() {
const messages = this.content && this.content._embedded && this.content._embedded.errors ? this.content._embedded.errors : []
return Array.isArray(messages) ? messages : [messages]
}
},
methods: {
onHide() {
setTimeout(() => {
this.$store.commit("core/setErrorMessage", undefined);
}, 1000);
}
}
};
</script> </script>
<style lang="scss"> <style lang="scss">
@import "../styles/variable"; @import "../styles/variable";

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="execution && outputs"> <div v-if="execution && outputs">
<b-navbar toggleable="lg" type="light" variant="light"> <b-navbar toggleable="lg" type="light" variant="light">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle> <b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-nav-form> <b-nav-form>
<v-select <v-select
@@ -10,7 +10,7 @@
@input="onSearch" @input="onSearch"
:options="selectOptions" :options="selectOptions"
:placeholder="$t('display output for specific task') + '...'" :placeholder="$t('display output for specific task') + '...'"
></v-select> />
</b-nav-form> </b-nav-form>
</b-collapse> </b-collapse>
</b-navbar> </b-navbar>
@@ -39,96 +39,96 @@
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import md5 from "md5"; import md5 from "md5";
import VarValue from "./VarValue"; import VarValue from "./VarValue";
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
export default { export default {
components: { components: {
VarValue, VarValue,
}, },
data() { data() {
return { return {
filter: "" filter: ""
}; };
}, },
created() { created() {
if (this.$route.query.search) { if (this.$route.query.search) {
this.filter = this.$route.query.search || "" this.filter = this.$route.query.search || ""
}
},
watch: {
$route() {
if (this.$route.query.search !== this.filter) {
this.filter = this.$route.query.search || "";
}
}
},
methods: {
onSearch() {
if (this.filter && this.$route.query.search !== this.filter) {
const newRoute = { query: { ...this.$route.query } };
newRoute.query.search = this.filter;
this.$router.push(newRoute);
} }
}, },
taskRunOutputToken(taskRun) { watch: {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: '')); $route() {
} if (this.$route.query.search !== this.filter) {
}, this.filter = this.$route.query.search || "";
computed: {
...mapState("execution", ["execution"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
options[this.taskRunOutputToken(taskRun)] = {
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ''),
value: this.taskRunOutputToken(taskRun)
} }
} }
return Object.values(options);
}, },
fields() { methods: {
return [ onSearch() {
{ if (this.filter && this.$route.query.search !== this.filter) {
key: "task", const newRoute = {query: {...this.$route.query}};
label: this.$t("task") newRoute.query.search = this.filter;
}, this.$router.push(newRoute);
{
key: "value",
label: this.$t("value")
},
{
key: "key",
label: this.$t("name")
},
{
key: "output",
label: this.$t("output")
}
];
},
outputs() {
const outputs = [];
for (const taskRun of this.execution.taskRunList || []) {
const token = this.taskRunOutputToken(taskRun)
if (!this.filter || token === this.filter) {
Utils.executionVars(taskRun.outputs).forEach(output => {
const item = {
key: output.key,
output: output.value,
task: taskRun.taskId,
value: taskRun.value
};
outputs.push(item);
})
} }
},
taskRunOutputToken(taskRun) {
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""));
}
},
computed: {
...mapState("execution", ["execution"]),
selectOptions() {
const options = {};
for (const taskRun of this.execution.taskRunList || []) {
options[this.taskRunOutputToken(taskRun)] = {
label: taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""),
value: this.taskRunOutputToken(taskRun)
}
}
return Object.values(options);
},
fields() {
return [
{
key: "task",
label: this.$t("task")
},
{
key: "value",
label: this.$t("value")
},
{
key: "key",
label: this.$t("name")
},
{
key: "output",
label: this.$t("output")
}
];
},
outputs() {
const outputs = [];
for (const taskRun of this.execution.taskRunList || []) {
const token = this.taskRunOutputToken(taskRun)
if (!this.filter || token === this.filter) {
Utils.executionVars(taskRun.outputs).forEach(output => {
const item = {
key: output.key,
output: output.value,
task: taskRun.taskId,
value: taskRun.value
};
outputs.push(item);
})
}
}
return outputs;
} }
return outputs;
} }
} };
};
</script> </script>

View File

@@ -17,148 +17,148 @@
</b-card> </b-card>
</template> </template>
<script> <script>
import Gantt from "./Gantt"; import Gantt from "./Gantt";
import Overview from "./Overview"; import Overview from "./Overview";
import Logs from "../logs/Logs"; import Logs from "../logs/Logs";
import Topology from "./Topology"; import Topology from "./Topology";
import ExecutionOutput from "./ExecutionOutput"; import ExecutionOutput from "./ExecutionOutput";
import Trigger from "vue-material-design-icons/Cogs"; import Trigger from "vue-material-design-icons/Cogs";
import BottomLine from "../layout/BottomLine"; import BottomLine from "../layout/BottomLine";
import FlowActions from "../flows/FlowActions"; import FlowActions from "../flows/FlowActions";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
mixins: [RouteContext], mixins: [RouteContext],
components: { components: {
Overview, Overview,
BottomLine, BottomLine,
Trigger, Trigger,
Gantt, Gantt,
Logs, Logs,
Topology, Topology,
FlowActions, FlowActions,
ExecutionOutput ExecutionOutput
},
data() {
return {
sse: undefined
};
},
created() {
this.follow();
},
methods: {
follow() {
this.closeSSE();
this.$store
.dispatch("execution/followExecution", this.$route.params)
.then(sse => {
this.sse = sse;
sse.subscribe("", (data, event) => {
this.$store.commit("execution/setExecution", data);
if (this.$route.query.tab === 'topology') {
this.$store.dispatch('execution/loadTree', data)
}
if (event && event.lastEventId === "end") {
this.closeSSE();
}
});
});
}, },
closeSSE() { data() {
if (this.sse) {
this.sse.close();
this.sse = undefined;
}
},
setTab(tab) {
this.$store.commit("execution/setTask", undefined);
this.$router.push({
name: "executionEdit",
params: this.$route.params,
query: { tab }
});
}
},
computed: {
...mapState("execution", ["execution"]),
routeInfo() {
const ns = this.$route.params.namespace;
return { return {
title: this.$t("execution"), sse: undefined
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList",
query: {
namespace: ns
}
}
},
{
label: `${ns}.${this.$route.params.flowId}`,
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
}
}
},
{
label: this.$t("executions"),
link: {
name: 'flowEdit',
params: {
namespace: ns,
id: this.$route.params.flowId
},
query: {
tab: 'executions'
}
}
},
{
label: this.$route.params.id,
link: {
name: "executionEdit"
}
}
]
}; };
}, },
tabs() { created() {
const title = title => this.$t(title); this.follow();
return [ },
{ methods: {
tab: "overview", follow() {
title: title("overview") this.closeSSE();
},
{ this.$store
tab: "gantt", .dispatch("execution/followExecution", this.$route.params)
title: title("gantt") .then(sse => {
}, this.sse = sse;
{ sse.subscribe("", (data, event) => {
tab: "logs", this.$store.commit("execution/setExecution", data);
title: title("logs") if (this.$route.query.tab === "topology") {
}, this.$store.dispatch("execution/loadTree", data)
{ }
tab: "topology", if (event && event.lastEventId === "end") {
title: title("topology") this.closeSSE();
}, }
{ });
tab: "execution-output", });
title: title("output") },
closeSSE() {
if (this.sse) {
this.sse.close();
this.sse = undefined;
} }
]; },
setTab(tab) {
this.$store.commit("execution/setTask", undefined);
this.$router.push({
name: "executionEdit",
params: this.$route.params,
query: {tab}
});
}
},
computed: {
...mapState("execution", ["execution"]),
routeInfo() {
const ns = this.$route.params.namespace;
return {
title: this.$t("execution"),
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList",
query: {
namespace: ns
}
}
},
{
label: `${ns}.${this.$route.params.flowId}`,
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
}
}
},
{
label: this.$t("executions"),
link: {
name: "flowEdit",
params: {
namespace: ns,
id: this.$route.params.flowId
},
query: {
tab: "executions"
}
}
},
{
label: this.$route.params.id,
link: {
name: "executionEdit"
}
}
]
};
},
tabs() {
const title = title => this.$t(title);
return [
{
tab: "overview",
title: title("overview")
},
{
tab: "gantt",
title: title("gantt")
},
{
tab: "logs",
title: title("logs")
},
{
tab: "topology",
title: title("topology")
},
{
tab: "execution-output",
title: title("output")
}
];
}
},
beforeDestroy() {
this.closeSSE();
this.$store.commit("execution/setExecution", undefined);
} }
}, };
beforeDestroy() {
this.closeSSE();
this.$store.commit("execution/setExecution", undefined);
}
};
</script> </script>

View File

@@ -3,10 +3,10 @@
<data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total"> <data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total">
<template v-slot:navbar> <template v-slot:navbar>
<search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields" /> <search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields" />
<namespace-select data-type="flow" v-if="$route.name !== 'flowEdit'" @onNamespaceSelect="onNamespaceSelect" /> <namespace-select data-type="flow" v-if="$route.name !== 'flowEdit'" @onNamespaceSelect="onNamespaceSelect" />
<status-filter-buttons @onRefresh="onStatusChange"/> <status-filter-buttons @onRefresh="onStatusChange" />
<date-range @onDate="onSearch" /> <date-range @onDate="onSearch" />
<refresh-button class="float-right" @onRefresh="loadData"/> <refresh-button class="float-right" @onRefresh="loadData" />
</template> </template>
<template v-slot:top> <template v-slot:top>
@@ -30,7 +30,7 @@
@row-dblclicked="onRowDoubleClick" @row-dblclicked="onRowDoubleClick"
> >
<template #empty> <template #empty>
<span class="text-black-50">{{$t('no result')}}</span> <span class="text-black-50">{{ $t('no result') }}</span>
</template> </template>
<template v-slot:cell(details)="row"> <template v-slot:cell(details)="row">
@@ -40,13 +40,15 @@
</template> </template>
<template <template
v-slot:cell(state.startDate)="row" v-slot:cell(state.startDate)="row"
>{{row.item.state.startDate | date('LLLL')}}</template> >
{{ row.item.state.startDate | date('LLLL') }}
</template>
<template <template
v-slot:cell(state.endDate)="row" v-slot:cell(state.endDate)="row"
> >
<span v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)"> <span v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)">
{{row.item.state.endDate | date('LLLL')}} {{ row.item.state.endDate | date('LLLL') }}
</span> </span>
</template> </template>
<template v-slot:cell(state.current)="row"> <template v-slot:cell(state.current)="row">
<status <status
@@ -55,197 +57,197 @@
size="sm" size="sm"
/> />
</template> </template>
<template v-slot:cell(state.duration)="row"> <template v-slot:cell(state.duration)="row">
<span v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)">{{durationFrom(row.item) | humanizeDuration}}</span> <span v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)">{{ durationFrom(row.item) | humanizeDuration }}</span>
<span v-else>{{row.item.state.duration | humanizeDuration}}</span> <span v-else>{{ row.item.state.duration | humanizeDuration }}</span>
</template> </template>
<template v-slot:cell(flowId)="row"> <template v-slot:cell(flowId)="row">
<router-link <router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}" :to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}"
>{{row.item.flowId}}</router-link> >
{{ row.item.flowId }}
</router-link>
</template> </template>
<template v-slot:cell(id)="row"> <template v-slot:cell(id)="row">
<code>{{row.item.id | id}}</code> <code>{{ row.item.id | id }}</code>
</template> </template>
<template v-slot:cell(trigger)="row"> <template v-slot:cell(trigger)="row">
<trigger-avatar @showTriggerDetails="showTriggerDetails" :execution="row.item"/> <trigger-avatar @showTriggerDetails="showTriggerDetails" :execution="row.item" />
</template> </template>
</b-table> </b-table>
</template> </template>
</data-table> </data-table>
<flow-trigger-details-modal v-if="flowTriggerDetails" :trigger="flowTriggerDetails"/> <flow-trigger-details-modal v-if="flowTriggerDetails" :trigger="flowTriggerDetails" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import DataTable from "../layout/DataTable"; import DataTable from "../layout/DataTable";
import Eye from "vue-material-design-icons/Eye"; import Eye from "vue-material-design-icons/Eye";
import Status from "../Status"; import Status from "../Status";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions"; import DataTableActions from "../../mixins/dataTableActions";
import SearchField from "../layout/SearchField"; import SearchField from "../layout/SearchField";
import NamespaceSelect from "../namespace/NamespaceSelect"; import NamespaceSelect from "../namespace/NamespaceSelect";
import DateRange from "../layout/DateRange"; import DateRange from "../layout/DateRange";
import RefreshButton from '../layout/RefreshButton' import RefreshButton from "../layout/RefreshButton"
import StatusFilterButtons from '../layout/StatusFilterButtons' import StatusFilterButtons from "../layout/StatusFilterButtons"
import StateGlobalChart from "../../components/stats/StateGlobalChart"; import StateGlobalChart from "../../components/stats/StateGlobalChart";
import FlowTriggerDetailsModal from "../../components/flows/TriggerDetailsModal"; import FlowTriggerDetailsModal from "../../components/flows/TriggerDetailsModal";
import TriggerAvatar from "../../components/flows/TriggerAvatar"; import TriggerAvatar from "../../components/flows/TriggerAvatar";
export default { export default {
mixins: [RouteContext, DataTableActions], mixins: [RouteContext, DataTableActions],
components: { components: {
Status, Status,
Eye, Eye,
DataTable, DataTable,
SearchField, SearchField,
NamespaceSelect, NamespaceSelect,
DateRange, DateRange,
RefreshButton, RefreshButton,
StatusFilterButtons, StatusFilterButtons,
StateGlobalChart, StateGlobalChart,
FlowTriggerDetailsModal, FlowTriggerDetailsModal,
TriggerAvatar TriggerAvatar
}, },
data() { data() {
return { return {
dataType: "execution", dataType: "execution",
dailyReady: false, dailyReady: false,
flowTriggerDetails: undefined flowTriggerDetails: undefined
};
},
beforeCreate() {
const q = JSON.parse(localStorage.getItem('executionQueries') || '{}')
q.sort = q.sort ? q.sort : 'state.startDate:desc'
q.status = q.status ? q.status : 'ALL'
localStorage.setItem('executionQueries', JSON.stringify(q))
},
computed: {
...mapState("execution", ["executions", "total"]),
...mapState("stat", ["daily"]),
fields() {
const title = title => {
return this.$t(title);
}; };
return [ },
{ beforeCreate() {
key: "id", const q = JSON.parse(localStorage.getItem("executionQueries") || "{}")
label: title("id") q.sort = q.sort ? q.sort : "state.startDate:desc"
}, q.status = q.status ? q.status : "ALL"
{ localStorage.setItem("executionQueries", JSON.stringify(q))
key: "state.startDate", },
label: title("start date"), computed: {
sortable: true ...mapState("execution", ["executions", "total"]),
}, ...mapState("stat", ["daily"]),
{ fields() {
key: "state.endDate", const title = title => {
label: title("end date"), return this.$t(title);
sortable: true };
}, return [
{ {
key: "state.duration", key: "id",
label: title("duration"), label: title("id")
sortable: true },
}, {
{ key: "state.startDate",
key: "namespace", label: title("start date"),
label: title("namespace"), sortable: true
sortable: true },
}, {
{ key: "state.endDate",
key: "flowId", label: title("end date"),
label: title("flow"), sortable: true
sortable: true },
}, {
{ key: "state.duration",
key: "state.current", label: title("duration"),
label: title("state"), sortable: true
class: "text-center", },
sortable: true {
}, key: "namespace",
{ label: title("namespace"),
key: "trigger", sortable: true
label: title("trigger"), },
class: "shrink" {
}, key: "flowId",
{ label: title("flow"),
key: "details", sortable: true
label: "", },
class: "row-action" {
key: "state.current",
label: title("state"),
class: "text-center",
sortable: true
},
{
key: "trigger",
label: title("trigger"),
class: "shrink"
},
{
key: "details",
label: "",
class: "row-action"
}
];
},
executionQuery() {
let filter;
if (this.$route.name === "flowEdit") {
filter = `namespace:${this.$route.params.namespace} AND flowId:${this.$route.params.id}`;
} }
];
},
executionQuery() {
let filter;
if (this.$route.name === "flowEdit") { return this.query + (filter ? " " + filter : "");
filter = `namespace:${this.$route.params.namespace} AND flowId:${this.$route.params.id}`; },
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
} }
},
methods: {
onStatusChange() {
this.saveFilters()
this.loadData()
},
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show("modal-triggers-details")
},
triggerExecution() {
this.$store
.dispatch("execution/triggerExecution", this.$route.params)
.then(response => {
this.$router.push({
name: "execution",
params: response.data
});
return this.query + (filter ? " " + filter : ""); return response.data
}, })
endDate() { .then((execution) => {
return new Date(); this.$toast().success(this.$t("triggered done", {name: execution.id}));
}, })
startDate() { },
return this.$moment(this.endDate) loadData(callback) {
.add(-30, "days") this.dailyReady = false;
.toDate(); this.$store
} .dispatch("stat/daily", {
}, q: this.executionQuery,
methods: { startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
onStatusChange() { endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
this.saveFilters() })
this.loadData() .then(() => {
}, this.dailyReady = true;
showTriggerDetails(trigger) {
this.flowTriggerDetails = trigger
this.$bvModal.show('modal-triggers-details')
},
triggerExecution() {
this.$store
.dispatch("execution/triggerExecution", this.$route.params)
.then(response => {
this.$router.push({
name: "execution",
params: response.data
}); });
return response.data
}) this.$store.dispatch("execution/findExecutions", {
.then((execution) => { size: parseInt(this.$route.query.size || 25),
this.$toast().success(this.$t('triggered done', {name: execution.id})); page: parseInt(this.$route.query.page || 1),
})
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
q: this.executionQuery, q: this.executionQuery,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'), sort: this.$route.query.sort,
endDate: this.$moment(this.endDate).format('YYYY-MM-DD') state: this.$route.query.status
}) }).finally(callback);
.then(() => { },
this.dailyReady = true; durationFrom(item) {
}); return (+new Date() - new Date(item.state.startDate).getTime()) / 1000
},
}
this.$store.dispatch("execution/findExecutions", { };
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
q: this.executionQuery,
sort: this.$route.query.sort,
state: this.$route.query.status
}).finally(callback);
},
durationFrom(item) {
return (+new Date() - new Date(item.state.startDate).getTime()) / 1000
},
}
};
</script> </script>

View File

@@ -5,7 +5,9 @@
<thead> <thead>
<tr class="bg-light"> <tr class="bg-light">
<th>{{ duration }}</th> <th>{{ duration }}</th>
<td v-for="(date, i) in dates" :key="i">{{ date }}</td> <td v-for="(date, i) in dates" :key="i">
{{ date }}
</td>
</tr> </tr>
</thead> </thead>
<tbody v-for="taskItem in series" :key="taskItem.id"> <tbody v-for="taskItem in series" :key="taskItem.id">
@@ -18,13 +20,15 @@
:target="`task-title-wrapper-${taskItem.id}`" :target="`task-title-wrapper-${taskItem.id}`"
> >
<code>{{ taskItem.name }}</code> <code>{{ taskItem.name }}</code>
<span v-if="taskItem.task && taskItem.task.value"><br/>{{ taskItem.task.value }}</span> <span v-if="taskItem.task && taskItem.task.value"><br>{{ taskItem.task.value }}</span>
</b-tooltip> </b-tooltip>
</th> </th>
<td :colspan="dates.length"> <td :colspan="dates.length">
<b-tooltip <b-tooltip
:target="`task-progress-${taskItem.id}`" :target="`task-progress-${taskItem.id}`"
placement="left"><span v-html="taskItem.tooltip"></span> placement="left"
>
<span v-html="taskItem.tooltip" />
</b-tooltip> </b-tooltip>
<div <div
:style="{left: Math.max(1, (taskItem.start - 1)) + '%', width: taskItem.width - 1 + '%'}" :style="{left: Math.max(1, (taskItem.start - 1)) + '%', width: taskItem.width - 1 + '%'}"
@@ -33,18 +37,19 @@
:id="`task-progress-${taskItem.id}`" :id="`task-progress-${taskItem.id}`"
> >
<div class="progress"> <div class="progress">
<div class="progress-bar" <div
:style="{left: taskItem.left + '%', width: (100-taskItem.left) + '%'}" class="progress-bar"
:class="'bg-' + taskItem.color + (taskItem.running ? ' progress-bar-striped' : '')" :style="{left: taskItem.left + '%', width: (100-taskItem.left) + '%'}"
role="progressbar"> :class="'bg-' + taskItem.color + (taskItem.running ? ' progress-bar-striped' : '')"
</div> role="progressbar"
/>
</div> </div>
</div> </div>
</td> </td>
</tr> </tr>
<tr v-if="task && task.id === taskItem.id"> <tr v-if="task && task.id === taskItem.id">
<td :colspan="dates.length + 1"> <td :colspan="dates.length + 1">
<log-list :task-run-id="task.id" level="TRACE"/> <log-list :task-run-id="task.id" level="TRACE" />
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -2,9 +2,9 @@
<div v-if="execution"> <div v-if="execution">
<b-row class="mb-3 text-right"> <b-row class="mb-3 text-right">
<b-col> <b-col>
<restart :execution="execution" @restart="restart"/> <restart :execution="execution" @restart="restart" />
<kill :execution="execution" /> <kill :execution="execution" />
<status :status="execution.state.current"/> <status :status="execution.state.current" />
</b-col> </b-col>
</b-row> </b-row>
<b-table responsive="xl" striped hover bordered :items="items" class="mb-0"> <b-table responsive="xl" striped hover bordered :items="items" class="mb-0">
@@ -12,7 +12,8 @@
<router-link <router-link
v-if="row.item.link" v-if="row.item.link"
:to="{name: 'executionEdit', params: row.item.link}" :to="{name: 'executionEdit', params: row.item.link}"
>{{ row.item.value }} >
{{ row.item.value }}
</router-link> </router-link>
<span v-else> <span v-else>
<span v-if="row.item.key === $t('revision')"> <span v-if="row.item.key === $t('revision')">
@@ -27,17 +28,17 @@
<div v-if="execution.trigger" class="mt-4"> <div v-if="execution.trigger" class="mt-4">
<h5>{{ $t('trigger') }}</h5> <h5>{{ $t('trigger') }}</h5>
<vars :execution="execution" :data="execution.trigger"/> <vars :execution="execution" :data="execution.trigger" />
</div> </div>
<div v-if="execution.inputs" class="mt-4"> <div v-if="execution.inputs" class="mt-4">
<h5>{{ $t('inputs') }}</h5> <h5>{{ $t('inputs') }}</h5>
<vars :execution="execution" :data="inputs"/> <vars :execution="execution" :data="inputs" />
</div> </div>
<div v-if="execution.variables" class="mt-4"> <div v-if="execution.variables" class="mt-4">
<h5>{{ $t('variables') }}</h5> <h5>{{ $t('variables') }}</h5>
<vars :execution="execution" :data="execution.variables"/> <vars :execution="execution" :data="execution.variables" />
</div> </div>
</div> </div>
</template> </template>
@@ -78,8 +79,8 @@
items() { items() {
const startTs = this.execution.state.histories[0].date; const startTs = this.execution.state.histories[0].date;
const stopTs = this.execution.state.histories[ const stopTs = this.execution.state.histories[
this.execution.state.histories.length - 1 this.execution.state.histories.length - 1
].date; ].date;
const delta = ts(stopTs) - ts(startTs); const delta = ts(stopTs) - ts(startTs);
const duration = this.$moment.duration(delta); const duration = this.$moment.duration(delta);
const humanDuration = humanizeDuration(duration); const humanDuration = humanizeDuration(duration);
@@ -94,8 +95,8 @@
key: this.$t("revision"), key: this.$t("revision"),
value: this.execution.flowRevision value: this.execution.flowRevision
}, },
{key: this.$t("created date"), value: this.$moment(startTs).format('LLLL')}, {key: this.$t("created date"), value: this.$moment(startTs).format("LLLL")},
{key: this.$t("updated date"), value: this.$moment(stopTs).format('LLLL')}, {key: this.$t("updated date"), value: this.$moment(stopTs).format("LLLL")},
{key: this.$t("duration"), value: humanDuration}, {key: this.$t("duration"), value: humanDuration},
{key: this.$t("steps"), value: stepCount} {key: this.$t("steps"), value: stepCount}
]; ];

View File

@@ -1,15 +1,15 @@
<template> <template>
<span> <span>
<b-button <b-button
@click="restart" @click="restart"
v-if="enabled" v-if="enabled"
:class="!isButtonGroup ? 'rounded-lg btn-info restart mr-1' : ''" :class="!isButtonGroup ? 'rounded-lg btn-info restart mr-1' : ''"
:title="$t('restart')" :title="$t('restart')"
> >
<restart-icon/> <restart-icon />
{{ (isButtonGroup ? '' : $t("restart")) }} {{ (isButtonGroup ? '' : $t("restart")) }}
</b-button> </b-button>
</span> </span>
</template> </template>
<script> <script>
import RestartIcon from "vue-material-design-icons/Restart"; import RestartIcon from "vue-material-design-icons/Restart";
@@ -30,7 +30,8 @@
}, },
task: { task: {
type: Object, type: Object,
required: false required: false,
default: undefined
} }
}, },
methods: { methods: {
@@ -43,9 +44,9 @@
taskId: this.task ? this.task.taskId : null taskId: this.task ? this.task.taskId : null
}) })
.then(response => { .then(response => {
this.$store.commit('execution/setExecution', response.data); this.$store.commit("execution/setExecution", response.data);
this.$router.push({name: 'executionEdit', params: response.data}); this.$router.push({name: "executionEdit", params: response.data});
this.$emit('restart') this.$emit("restart")
}) })
.then(() => { .then(() => {
this.$toast().success(this.$t("restarted")); this.$toast().success(this.$t("restarted"));

View File

@@ -4,36 +4,36 @@
<topology-tree <topology-tree
ref="topology" ref="topology"
v-if="execution && dataTree" v-if="execution && dataTree"
:dataTree="dataTree" :data-tree="dataTree"
:label="getLabel" :label="getLabel"
/> />
</b-col> </b-col>
</b-row> </b-row>
</template> </template>
<script> <script>
import TopologyTree from "../graph/TopologyTree"; import TopologyTree from "../graph/TopologyTree";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
components: { components: {
TopologyTree TopologyTree
},
computed: {
...mapState("execution", ["execution", "dataTree"])
},
created() {
if (!this.dataTree && this.execution) {
this.$store.dispatch('execution/loadTree', this.execution)
}
},
methods: {
getLabel(node) {
return node.data.taskId;
}, },
update() { computed: {
if (this.$refs.topology) { ...mapState("execution", ["execution", "dataTree"])
this.$refs.topology.update(); },
created() {
if (!this.dataTree && this.execution) {
this.$store.dispatch("execution/loadTree", this.execution)
}
},
methods: {
getLabel(node) {
return node.data.taskId;
},
update() {
if (this.$refs.topology) {
this.$refs.topology.update();
}
} }
} }
} };
};
</script> </script>

View File

@@ -3,9 +3,10 @@
v-if="isFile(value)" v-if="isFile(value)"
target="_blank" target="_blank"
:href="itemUrl(value)" :href="itemUrl(value)"
><download /> {{ $t('download') }} >
<download /> {{ $t('download') }}
</b-link> </b-link>
<span v-else v-html="value"></span> <span v-else v-html="value" />
</template> </template>
<script> <script>
@@ -26,11 +27,13 @@
}, },
props: { props: {
value: { value: {
type: Object,
required: true required: true
}, },
execution: { execution: {
type: Object, type: Object,
required: false required: false,
default: undefined
} }
} }
}; };

View File

@@ -15,13 +15,15 @@
<template v-slot:thead-top v-if="title"> <template v-slot:thead-top v-if="title">
<b-tr class="top"> <b-tr class="top">
<b-th colspan="2">{{ title }}</b-th> <b-th colspan="2">
{{ title }}
</b-th>
</b-tr> </b-tr>
</template> </template>
<template v-slot:empty> <template v-slot:empty>
<div class="alert alert-info mb-0" role="alert"> <div class="alert alert-info mb-0" role="alert">
{{ $t("no data current task")}} {{ $t("no data current task") }}
</div> </div>
</template> </template>
@@ -46,11 +48,13 @@
}, },
title: { title: {
type: String, type: String,
required: false required: false,
default: undefined
}, },
execution: { execution: {
type: Object, type: Object,
required: false required: false,
default: undefined
} }
}, },
computed: { computed: {

View File

@@ -1,17 +1,17 @@
<template> <template>
<b-row> <b-row>
<b-col> <b-col>
<flow-edit v-if="flow" :prevent-route-info="true"/> <flow-edit v-if="flow" :prevent-route-info="true" />
</b-col> </b-col>
</b-row> </b-row>
</template> </template>
<script> <script>
import { mapGetters } from "vuex"; import {mapGetters} from "vuex";
import FlowEdit from "./FlowEdit"; import FlowEdit from "./FlowEdit";
export default { export default {
components: { FlowEdit }, components: {FlowEdit},
computed: { computed: {
...mapGetters("flow", ["flow"]) ...mapGetters("flow", ["flow"])
} }
}; };
</script> </script>

View File

@@ -1,64 +1,65 @@
<template> <template>
<div class="container" v-if="flow"> <div class="container" v-if="flow">
<b-form v-hotkey="keymap" @submit.prevent="onSubmit"> <b-form v-hotkey="keymap" @submit.prevent="onSubmit">
<b-alert v-if="flow.triggers" variant="warning" show>{{$t('warning flow with triggers')}}</b-alert> <b-alert v-if="flow.triggers" variant="warning" show>
{{ $t('warning flow with triggers') }}
</b-alert>
<b-form-group <b-form-group
v-for="input in flow.inputs" v-for="input in flow.inputs"
:key="input.id" :key="input.id"
:label="input.name" :label="input.name"
label-cols-sm="2" label-cols-sm="2"
label-align-sm="right" label-align-sm="right"
label-size="sm" label-size="sm"
> >
<b-form-input <b-form-input
v-if="input.type === 'STRING'" v-if="input.type === 'STRING'"
v-model="input.value" v-model="input.value"
type="text" type="text"
:required="input.required" :required="input.required"
:placeholder="`${placeholder} ${input.name}`" :placeholder="`${placeholder} ${input.name}`"
></b-form-input> />
<b-form-input <b-form-input
v-if="input.type === 'INT'" v-if="input.type === 'INT'"
v-model="input.value" v-model="input.value"
type="number" type="number"
step="1" step="1"
:required="input.required" :required="input.required"
:placeholder="`${placeholder} ${input.name}`" :placeholder="`${placeholder} ${input.name}`"
></b-form-input> />
<b-form-input <b-form-input
v-if="input.type === 'FLOAT'" v-if="input.type === 'FLOAT'"
v-model="input.value" v-model="input.value"
type="number" type="number"
step="0.001" step="0.001"
:required="input.required" :required="input.required"
:placeholder="`${placeholder} ${input.name}`" :placeholder="`${placeholder} ${input.name}`"
></b-form-input> />
<date-picker <date-picker
v-if="input.type === 'DATETIME'" v-if="input.type === 'DATETIME'"
v-model="input.value" v-model="input.value"
:required="input.required" :required="input.required"
type="datetime" type="datetime"
class="w-100" class="w-100"
:placeholder="$t('select datetime')" :placeholder="$t('select datetime')"
></date-picker> />
<b-form-file <b-form-file
v-if="input.type === 'FILE'" v-if="input.type === 'FILE'"
v-model="input.value" v-model="input.value"
:required="input.required" :required="input.required"
:state="Boolean(input.value)" :state="Boolean(input.value)"
:placeholder="$t('choose file')" :placeholder="$t('choose file')"
></b-form-file> />
</b-form-group> </b-form-group>
<b-form-group class="text-right mb-0"> <b-form-group class="text-right mb-0">
<b-button type="submit" variant="primary"> <b-button type="submit" variant="primary">
{{$t('launch execution')}} {{ $t('launch execution') }}
<trigger title/> <trigger title />
</b-button> </b-button>
</b-form-group> </b-form-group>
</b-form> </b-form>
<br/> <br>
<b-card :header="$t('triggers')" v-if="flow && flow.triggers"> <b-card :header="$t('triggers')" v-if="flow && flow.triggers">
<triggers /> <triggers />
</b-card> </b-card>
@@ -79,7 +80,7 @@
}, },
keymap () { keymap () {
return { return {
'ctrl+enter': this.onSubmit, "ctrl+enter": this.onSubmit,
} }
} }
}, },
@@ -110,13 +111,13 @@
formData formData
}) })
.then(response => { .then(response => {
this.$store.commit('execution/setExecution', response.data) this.$store.commit("execution/setExecution", response.data)
this.$router.push({name: 'executionEdit', params: response.data}) this.$router.push({name: "executionEdit", params: response.data})
return response.data; return response.data;
}) })
.then((execution) => { .then((execution) => {
this.$toast().success(this.$t('triggered done', {name: execution.id})); this.$toast().success(this.$t("triggered done", {name: execution.id}));
}) })
} }
} }

View File

@@ -3,45 +3,45 @@
<b-dropdown-item> <b-dropdown-item>
<router-link :to="`/flows/edit/${actionFlow.namespace}/${actionFlow.id}`"> <router-link :to="`/flows/edit/${actionFlow.namespace}/${actionFlow.id}`">
<edit /> <edit />
{{$t('edit flow')}} {{actionFlow.id}} {{ $t('edit flow') }} {{ actionFlow.id }}
</router-link> </router-link>
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item> <b-dropdown-item>
<router-link :to="`/executions/${actionFlow.namespace}/${actionFlow.id}`"> <router-link :to="`/executions/${actionFlow.namespace}/${actionFlow.id}`">
<search /> <search />
{{$tc('display flow {id} executions', null, flow)}} {{ $tc('display flow {id} executions', null, flow) }}
</router-link> </router-link>
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item> <b-dropdown-item>
<router-link :to="{name: 'flowTopology', params: actionFlow}"> <router-link :to="{name: 'flowTopology', params: actionFlow}">
<graph /> <graph />
{{$t('display topology for flow')}} {{actionFlow.id}} {{ $t('display topology for flow') }} {{ actionFlow.id }}
</router-link> </router-link>
</b-dropdown-item> </b-dropdown-item>
</b-dropdown> </b-dropdown>
</template> </template>
<script> <script>
import Search from "vue-material-design-icons/Magnify"; import Search from "vue-material-design-icons/Magnify";
import Edit from "vue-material-design-icons/Pencil"; import Edit from "vue-material-design-icons/Pencil";
import Graph from "vue-material-design-icons/Graph"; import Graph from "vue-material-design-icons/Graph";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
components: { components: {
Search, Search,
Edit, Edit,
Graph Graph
}, },
props: { props: {
flowItem: { flowItem: {
type: Object, type: Object,
required: false default: undefined,
}
},
computed: {
...mapState("flow", ["flow"]),
actionFlow() {
return this.flow || this.flowItem;
}
} }
}, };
computed: {
...mapState("flow", ["flow"]),
actionFlow() {
return this.flow || this.flowItem;
}
}
};
</script> </script>

View File

@@ -1,17 +1,17 @@
<template> <template>
<div> <div>
<editor @onSave="save" v-model="content" lang="yaml"></editor> <editor @onSave="save" v-model="content" lang="yaml" />
<bottom-line v-if="canSave || canDelete"> <bottom-line v-if="canSave || canDelete">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<b-button class="btn-danger" v-if="canDelete" @click="deleteFile"> <b-button class="btn-danger" v-if="canDelete" @click="deleteFile">
<delete /> <delete />
<span>{{$t('delete')}}</span> <span>{{ $t('delete') }}</span>
</b-button> </b-button>
<b-button @click="save" v-if="canSave"> <b-button @click="save" v-if="canSave">
<content-save /> <content-save />
<span>{{$t('save')}}</span> <span>{{ $t('save') }}</span>
</b-button> </b-button>
</li> </li>
</ul> </ul>
@@ -20,21 +20,21 @@
</template> </template>
<script> <script>
import flowTemplateEdit from "../../mixins/flowTemplateEdit"; import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import { mapGetters } from "vuex"; import {mapGetters} from "vuex";
export default { export default {
mixins: [flowTemplateEdit], mixins: [flowTemplateEdit],
data() { data() {
return { return {
dataType: "flow", dataType: "flow",
}; };
}, },
computed: { computed: {
...mapGetters("flow", ["flow"]), ...mapGetters("flow", ["flow"]),
}, },
created() { created() {
this.loadFile(); this.loadFile();
}, },
}; };
</script> </script>

View File

@@ -20,139 +20,139 @@
</div> </div>
</template> </template>
<script> <script>
import Overview from "./Overview"; import Overview from "./Overview";
import Schedule from "./Schedule"; import Schedule from "./Schedule";
import DataSource from "./DataSource"; import DataSource from "./DataSource";
import Revisions from "./Revisions"; import Revisions from "./Revisions";
import ExecutionConfiguration from "./ExecutionConfiguration"; import ExecutionConfiguration from "./ExecutionConfiguration";
import BottomLine from "../layout/BottomLine"; import BottomLine from "../layout/BottomLine";
import FlowActions from "./FlowActions"; import FlowActions from "./FlowActions";
import Logs from "../logs/LogsWrapper"; import Logs from "../logs/LogsWrapper";
import Executions from "../executions/Executions"; import Executions from "../executions/Executions";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import { mapState } from "vuex"; import {mapState} from "vuex";
import permission from "../../models/permission"; import permission from "../../models/permission";
import action from "../../models/action"; import action from "../../models/action";
export default { export default {
mixins: [RouteContext], mixins: [RouteContext],
components: { components: {
Overview, Overview,
Schedule, Schedule,
BottomLine, BottomLine,
DataSource, DataSource,
FlowActions, FlowActions,
Executions, Executions,
ExecutionConfiguration, ExecutionConfiguration,
Revisions, Revisions,
Logs Logs
},
created() {
this.$store.dispatch("flow/loadFlow", this.$route.params).then(() => {
if (this.flow) {
this.$store.dispatch("flow/loadTree", this.flow);
}
});
},
methods: {
setTab(tab) {
this.$router.push({
name: "flowEdit",
params: this.$route.params,
query: { tab }
});
}
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
routeInfo() {
return {
title: this.$route.params.id,
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList"
}
},
{
label: this.$route.params.namespace,
link: {
name: "flowsList",
query: {
namespace: this.$route.params.namespace
}
}
},
{
label: this.$route.params.id,
link: {
name: "flowEdit",
params: {
namespace: this.$route.params.namespace,
id: this.$route.params.id
}
}
}
]
};
}, },
tabs() { created() {
const title = title => this.$t(title); this.$store.dispatch("flow/loadFlow", this.$route.params).then(() => {
const tabs = [ if (this.flow) {
{ this.$store.dispatch("flow/loadTree", this.flow);
tab: "overview", }
title: title("overview") });
}, },
]; methods: {
setTab(tab) {
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.READ, this.flow.namespace)) { this.$router.push({
tabs.push({ name: "flowEdit",
tab: "executions", params: this.$route.params,
title: title("executions") query: {tab}
}); });
} }
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
routeInfo() {
return {
title: this.$route.params.id,
breadcrumb: [
{
label: this.$t("flows"),
link: {
name: "flowsList"
}
},
{
label: this.$route.params.namespace,
link: {
name: "flowsList",
query: {
namespace: this.$route.params.namespace
}
}
},
{
label: this.$route.params.id,
link: {
name: "flowEdit",
params: {
namespace: this.$route.params.namespace,
id: this.$route.params.id
}
}
}
]
};
},
tabs() {
const title = title => this.$t(title);
const tabs = [
{
tab: "overview",
title: title("overview")
},
];
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace)) { if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.READ, this.flow.namespace)) {
tabs.push({ tabs.push({
tab: "execution-configuration", tab: "executions",
title: title("launch execution") title: title("executions")
}); });
}
if (this.user && this.flow && this.user.isAllowed(permission.EXECUTION, action.CREATE, this.flow.namespace)) {
tabs.push({
tab: "execution-configuration",
title: title("launch execution")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.UPDATE, this.flow.namespace)) {
tabs.push({
tab: "data-source",
title: title("source"),
class: "p-0"
});
tabs.push({
tab: "schedule",
title: title("schedule"),
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "revisions",
title: title("revisions")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "logs",
title: title("logs")
});
}
return tabs;
} }
},
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.UPDATE, this.flow.namespace)) { destroyed () {
tabs.push({ this.$store.commit("flow/setFlow", undefined)
tab: "data-source",
title: title("source"),
class: "p-0"
});
tabs.push({
tab: "schedule",
title: title("schedule"),
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "revisions",
title: title("revisions")
});
}
if (this.user && this.flow && this.user.isAllowed(permission.FLOW, action.READ, this.flow.namespace)) {
tabs.push({
tab: "logs",
title: title("logs")
});
}
return tabs;
} }
}, };
destroyed () {
this.$store.commit('flow/setFlow', undefined)
}
};
</script> </script>

View File

@@ -37,7 +37,7 @@
show-empty show-empty
> >
<template #empty> <template #empty>
<span class="text-black-50">{{$t('no result')}}</span> <span class="text-black-50">{{ $t('no result') }}</span>
</template> </template>
<template v-slot:cell(actions)="row"> <template v-slot:cell(actions)="row">
@@ -63,20 +63,21 @@
<template v-slot:cell(id)="row"> <template v-slot:cell(id)="row">
<router-link <router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.id}, query:{tab: 'executions'}}" :to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.id}, query:{tab: 'executions'}}"
>{{row.item.id}}</router-link> >
{{ row.item.id }}
</router-link>
&nbsp;<markdown-tooltip :id="row.item.namespace + '-' + row.item.id" :description="row.item.description" /> &nbsp;<markdown-tooltip :id="row.item.namespace + '-' + row.item.id" :description="row.item.description" />
</template> </template>
<template v-slot:cell(triggers)="row"> <template v-slot:cell(triggers)="row">
<trigger-avatar @showTriggerDetails="showTriggerDetails" :flow="row.item"/> <trigger-avatar @showTriggerDetails="showTriggerDetails" :flow="row.item" />
</template> </template>
</b-table> </b-table>
</template> </template>
</data-table> </data-table>
</div> </div>
<trigger-details-modal :trigger="flowTriggerDetails"/> <trigger-details-modal :trigger="flowTriggerDetails" />
<bottom-line v-if="user && user.hasAnyAction(permission.FLOW, action.CREATE)"> <bottom-line v-if="user && user.hasAnyAction(permission.FLOW, action.CREATE)">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
@@ -84,7 +85,7 @@
<router-link :to="{name: 'flowsAdd'}"> <router-link :to="{name: 'flowsAdd'}">
<b-button variant="primary"> <b-button variant="primary">
<plus /> <plus />
{{$t('create')}} {{ $t('create') }}
</b-button> </b-button>
</router-link> </router-link>
</li> </li>
@@ -94,156 +95,156 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import permission from "../../models/permission"; import permission from "../../models/permission";
import action from "../../models/action"; import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect"; import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus"; import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye"; import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine"; import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions"; import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable"; import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField"; import SearchField from "../layout/SearchField";
import StateChart from "../stats/StateChart"; import StateChart from "../stats/StateChart";
import DurationChart from "../stats/DurationChart"; import DurationChart from "../stats/DurationChart";
import StateGlobalChart from "../stats/StateGlobalChart"; import StateGlobalChart from "../stats/StateGlobalChart";
import TriggerDetailsModal from "./TriggerDetailsModal"; import TriggerDetailsModal from "./TriggerDetailsModal";
import TriggerAvatar from "./TriggerAvatar"; import TriggerAvatar from "./TriggerAvatar";
import MarkdownTooltip from "../layout/MarkdownTooltip" import MarkdownTooltip from "../layout/MarkdownTooltip"
export default { export default {
mixins: [RouteContext, DataTableActions], mixins: [RouteContext, DataTableActions],
components: { components: {
NamespaceSelect, NamespaceSelect,
BottomLine, BottomLine,
Plus, Plus,
Eye, Eye,
DataTable, DataTable,
SearchField, SearchField,
StateChart, StateChart,
DurationChart, DurationChart,
StateGlobalChart, StateGlobalChart,
TriggerDetailsModal, TriggerDetailsModal,
TriggerAvatar, TriggerAvatar,
MarkdownTooltip MarkdownTooltip
}, },
data() { data() {
return { return {
dataType: "flow", dataType: "flow",
permission: permission, permission: permission,
action: action, action: action,
dailyGroupByFlowReady: false, dailyGroupByFlowReady: false,
dailyReady: false, dailyReady: false,
flowTriggerDetails: undefined flowTriggerDetails: undefined
};
},
computed: {
...mapState("flow", ["flows", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = title => {
return this.$t(title);
}; };
return [
{
key: "id",
label: title("flow"),
sortable: true
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "state",
label: title("execution statistics"),
sortable: false,
class: "row-graph"
},
{
key: "duration",
label: title("duration"),
sortable: false,
class: "row-graph"
},
{
key: "triggers",
label: title("triggers"),
class: "shrink"
},
{
key: "actions",
label: "",
class: "row-action"
}
];
}, },
endDate() { computed: {
return new Date(); ...mapState("flow", ["flows", "total"]),
}, ...mapState("stat", ["dailyGroupByFlow", "daily"]),
startDate() { ...mapState("auth", ["user"]),
return this.$moment(this.endDate) fields() {
.add(-30, "days") const title = title => {
.toDate(); return this.$t(title);
} };
}, return [
methods: { {
showTriggerDetails(trigger) { key: "id",
this.flowTriggerDetails = trigger label: title("flow"),
this.$bvModal.show('modal-triggers-details') sortable: true
}, },
chartData(row) { {
if (this.dailyGroupByFlow && this.dailyGroupByFlow[row.item.namespace] && this.dailyGroupByFlow[row.item.namespace][row.item.id]) { key: "namespace",
return this.dailyGroupByFlow[row.item.namespace][row.item.id]; label: title("namespace"),
} else { sortable: true
return []; },
{
key: "state",
label: title("execution statistics"),
sortable: false,
class: "row-graph"
},
{
key: "duration",
label: title("duration"),
sortable: false,
class: "row-graph"
},
{
key: "triggers",
label: title("triggers"),
class: "shrink"
},
{
key: "actions",
label: "",
class: "row-action"
}
];
},
endDate() {
return new Date();
},
startDate() {
return this.$moment(this.endDate)
.add(-30, "days")
.toDate();
} }
}, },
loadData(callback) { methods: {
this.dailyReady = false; showTriggerDetails(trigger) {
this.$store this.flowTriggerDetails = trigger
.dispatch("stat/daily", { this.$bvModal.show("modal-triggers-details")
q: this.query.replace("id:" , "flowId:"), },
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'), chartData(row) {
endDate: this.$moment(this.endDate).format('YYYY-MM-DD') if (this.dailyGroupByFlow && this.dailyGroupByFlow[row.item.namespace] && this.dailyGroupByFlow[row.item.namespace][row.item.id]) {
}) return this.dailyGroupByFlow[row.item.namespace][row.item.id];
.then(() => { } else {
this.dailyReady = true; return [];
}); }
},
loadData(callback) {
this.dailyReady = false;
this.$store
.dispatch("stat/daily", {
q: this.query.replace("id:" , "flowId:"),
startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
})
.then(() => {
this.dailyReady = true;
});
this.$store this.$store
.dispatch("flow/findFlows", { .dispatch("flow/findFlows", {
q: this.query, q: this.query,
size: parseInt(this.$route.query.size || 25), size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1), page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort sort: this.$route.query.sort
}) })
.then(flows => { .then(flows => {
this.dailyGroupByFlowReady = false; this.dailyGroupByFlowReady = false;
callback(); callback();
if (flows.results && flows.results.length > 0) { if (flows.results && flows.results.length > 0) {
let query = "((" + flows.results let query = "((" + flows.results
.map(flow => "flowId:" + flow.id + " AND namespace:" + flow.namespace) .map(flow => "flowId:" + flow.id + " AND namespace:" + flow.namespace)
.join(") OR (") + "))" .join(") OR (") + "))"
this.$store this.$store
.dispatch("stat/dailyGroupByFlow", { .dispatch("stat/dailyGroupByFlow", {
q: query, q: query,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'), startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD') endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
}) })
.then(() => { .then(() => {
this.dailyGroupByFlowReady = true this.dailyGroupByFlowReady = true
}) })
} }
}) })
}
} }
} };
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/_variable.scss"; @import "../../styles/_variable.scss";

View File

@@ -2,36 +2,36 @@
<div> <div>
<b-row class="topology-wrapper"> <b-row class="topology-wrapper">
<b-col> <b-col>
<topology-tree :isFlow="true" v-if="flow && dataTree" :dataTree="dataTree" :label="getLabel"/> <topology-tree :is-flow="true" v-if="flow && dataTree" :data-tree="dataTree" :label="getLabel" />
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>
<b-col> <b-col>
<task-details/> <task-details />
</b-col> </b-col>
</b-row> </b-row>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import TopologyTree from "../graph/TopologyTree"; import TopologyTree from "../graph/TopologyTree";
import TaskDetails from "./TaskDetails"; import TaskDetails from "./TaskDetails";
export default { export default {
components: { components: {
TopologyTree, TopologyTree,
TaskDetails TaskDetails
}, },
computed: { computed: {
...mapState("flow", ["flow", "dataTree"]), ...mapState("flow", ["flow", "dataTree"]),
}, },
methods: { methods: {
getLabel (node) { getLabel (node) {
const id = node.data.id; const id = node.data.id;
return `${id.substr(0, 25)}${id.length > 25 ? "..." : ""}`; return `${id.substr(0, 25)}${id.length > 25 ? "..." : ""}`;
}
} }
} };
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.topology-wrapper { .topology-wrapper {

View File

@@ -2,20 +2,20 @@
<div v-if="revisions && revisions.length > 1"> <div v-if="revisions && revisions.length > 1">
<b-row> <b-row>
<b-col md="12"> <b-col md="12">
<b-form-select v-model="displayType" :options="displayTypes"></b-form-select> <b-form-select v-model="displayType" :options="displayTypes" />
<hr /> <hr>
</b-col> </b-col>
<b-col md="6"> <b-col md="6">
<b-form-select v-model="revisionLeft" :options="options"></b-form-select> <b-form-select v-model="revisionLeft" :options="options" />
</b-col> </b-col>
<b-col md="6"> <b-col md="6">
<b-form-select v-model="revisionRight" :options="options"></b-form-select> <b-form-select v-model="revisionRight" :options="options" />
</b-col> </b-col>
<b-col md="12"> <b-col md="12">
<br /> <br>
<code-diff <code-diff
:outputFormat="displayType" :output-format="displayType"
:old-string="revisionLeftText" :old-string="revisionLeftText"
:new-string="revisionRightText" :new-string="revisionRightText"
:context="10" :context="10"
@@ -24,98 +24,100 @@
</b-row> </b-row>
</div> </div>
<div v-else> <div v-else>
<b-alert class="mb-0" show>{{$t('no revisions found')}}</b-alert> <b-alert class="mb-0" show>
{{ $t('no revisions found') }}
</b-alert>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import YamlUtils from "../../utils/yamlUtils"; import YamlUtils from "../../utils/yamlUtils";
import CodeDiff from "vue-code-diff"; import CodeDiff from "vue-code-diff";
export default { export default {
components: { CodeDiff }, components: {CodeDiff},
created() { created() {
this.$store this.$store
.dispatch("flow/loadRevisions", this.$route.params) .dispatch("flow/loadRevisions", this.$route.params)
.then(() => { .then(() => {
const revisionLength = this.revisions.length; const revisionLength = this.revisions.length;
if (revisionLength > 0) { if (revisionLength > 0) {
this.revisionRight = revisionLength - 1; this.revisionRight = revisionLength - 1;
} }
if (revisionLength > 1) { if (revisionLength > 1) {
this.revisionLeft = revisionLength - 2; this.revisionLeft = revisionLength - 2;
} }
if (this.$route.query.revisionRight) { if (this.$route.query.revisionRight) {
this.revisionRight = this.revisionIndex( this.revisionRight = this.revisionIndex(
this.$route.query.revisionRight this.$route.query.revisionRight
); );
if ( if (
!this.$route.query.revisionLeft && !this.$route.query.revisionLeft &&
this.revisionRight > 0 this.revisionRight > 0
) { ) {
this.revisionLeft = this.revisions.length - 1; this.revisionLeft = this.revisions.length - 1;
}
}
if (this.$route.query.revisionLeft) {
this.revisionLeft = this.revisionIndex(
this.$route.query.revisionLeft
);
}
});
},
methods: {
revisionIndex(revision) {
const rev = parseInt(revision);
for (let i = 0; i < this.revisions.length; i++) {
if (rev === this.revisions[i].revision) {
return i;
} }
} }
if (this.$route.query.revisionLeft) { },
this.revisionLeft = this.revisionIndex( },
this.$route.query.revisionLeft computed: {
); ...mapState("flow", ["revisions"]),
options() {
return (this.revisions || []).map((revision, x) => {
return {
value: x,
text: revision.revision,
};
});
},
revisionLeftText() {
if (this.revisionLeft === undefined) {
return "";
} }
}); return YamlUtils.stringify(this.revisions[this.revisionLeft]);
}, },
methods: { revisionRightText() {
revisionIndex(revision) { if (this.revisionRight === undefined) {
const rev = parseInt(revision); return "";
for (let i = 0; i < this.revisions.length; i++) {
if (rev === this.revisions[i].revision) {
return i;
} }
} return YamlUtils.stringify(this.revisions[this.revisionRight]);
},
diff() {
const linesLeft = this.revisionLeftText.split("\n");
const linesRight = this.revisionRightText.split("\n");
const minLength = Math.min(linesLeft.length, linesRight.length);
const diff = [];
for (let i = 0; i < minLength; i++) {
diff.push(linesLeft[i] === linesRight[i] ? "" : "≠");
}
return diff.join("\n");
},
}, },
}, data() {
computed: { return {
...mapState("flow", ["revisions"]), revisionLeft: 0,
options() { revisionRight: 0,
return (this.revisions || []).map((revision, x) => { displayType: "side-by-side",
return { displayTypes: [
value: x, {value: "side-by-side", text: "side-by-side"},
text: revision.revision, {value: "line-by-line", text: "line-by-line"},
}; ],
}); };
}, },
revisionLeftText() { };
if (this.revisionLeft === undefined) {
return "";
}
return YamlUtils.stringify(this.revisions[this.revisionLeft]);
},
revisionRightText() {
if (this.revisionRight === undefined) {
return "";
}
return YamlUtils.stringify(this.revisions[this.revisionRight]);
},
diff() {
const linesLeft = this.revisionLeftText.split("\n");
const linesRight = this.revisionRightText.split("\n");
const minLength = Math.min(linesLeft.length, linesRight.length);
const diff = [];
for (let i = 0; i < minLength; i++) {
diff.push(linesLeft[i] === linesRight[i] ? "" : "≠");
}
return diff.join("\n");
},
},
data() {
return {
revisionLeft: 0,
revisionRight: 0,
displayType: "side-by-side",
displayTypes: [
{ value: "side-by-side", text: "side-by-side" },
{ value: "line-by-line", text: "line-by-line" },
],
};
},
};
</script> </script>

View File

@@ -6,21 +6,29 @@
@set="set" @set="set"
:schedule="schedule" :schedule="schedule"
:index="x" :index="x"
v-for="(schedule, x) in (flow.triggers || []).filter(r => r.type === 'org.kestra.core.models.triggers.types.Schedule')" v-for="(schedule, x) in (flow.triggers || []).filter(
(r) =>
r.type ===
'org.kestra.core.models.triggers.types.Schedule'
)"
:key="x" :key="x"
/> />
</b-list-group> </b-list-group>
<bottom-line v-if="canSave"> <bottom-line v-if="canSave">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<b-button variant="primary" @click="addSchedule" v-if="canSave"> <b-button
variant="primary"
@click="addSchedule"
v-if="canSave"
>
<plus /> <plus />
{{ $t('add schedule') }} {{ $t("add schedule") }}
</b-button> </b-button>
<b-button @click="save" v-if="canSave"> <b-button @click="save" v-if="canSave">
<content-save /> <content-save />
<span>{{$t('save')}}</span> <span>{{ $t("save") }}</span>
</b-button> </b-button>
</li> </li>
</ul> </ul>
@@ -28,46 +36,49 @@
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import ContentSave from "vue-material-design-icons/ContentSave"; import ContentSave from "vue-material-design-icons/ContentSave";
import Plus from "vue-material-design-icons/Plus"; import Plus from "vue-material-design-icons/Plus";
import ScheduleItem from "./ScheduleItem"; import ScheduleItem from "./ScheduleItem";
import BottomLine from "../layout/BottomLine"; import BottomLine from "../layout/BottomLine";
import { canSaveFlowTemplate, saveFlowTemplate } from "../../utils/flowTemplate"; import {
canSaveFlowTemplate,
saveFlowTemplate,
} from "../../utils/flowTemplate";
export default { export default {
components: { components: {
Plus, Plus,
ContentSave, ContentSave,
ScheduleItem, ScheduleItem,
BottomLine BottomLine,
},
computed: {
...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
canSave() {
return canSaveFlowTemplate(true, this.user, this.flow, "flow");
}
},
methods: {
save() {
saveFlowTemplate(this, this.flow, "flow");
}, },
set(index, schedule) { computed: {
this.$store.commit("flow/setTrigger", {index, trigger: schedule}); ...mapState("flow", ["flow"]),
...mapState("auth", ["user"]),
canSave() {
return canSaveFlowTemplate(true, this.user, this.flow, "flow");
},
}, },
remove(index) { methods: {
this.$store.commit("flow/removeTrigger", index); save() {
saveFlowTemplate(this, this.flow, "flow");
},
set(index, schedule) {
this.$store.commit("flow/setTrigger", {index, trigger: schedule});
},
remove(index) {
this.$store.commit("flow/removeTrigger", index);
},
addSchedule() {
this.$store.commit("flow/addTrigger", {
id: "schedule",
cron: "0 4 * * 1,4",
type: "org.kestra.core.models.triggers.types.Schedule",
});
},
}, },
addSchedule() { };
this.$store.commit("flow/addTrigger", {
id: "schedule",
cron: "0 4 * * 1,4",
type: "org.kestra.core.models.triggers.types.Schedule",
});
}
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
</style> </style>

View File

@@ -1,24 +1,29 @@
<template> <template>
<b-list-group-item> <b-list-group-item>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('id')" :label-for="'input-id-' + index"> <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-input required :id="'input-id-' + index" v-model="schedule.id" />
</b-form-group> </b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2" <b-form-group
:label-for="'input-cron-' + index" label-cols-sm="3"
:state="isValid"> label-cols-lg="2"
:label-for="'input-cron-' + index"
:state="isValid"
>
<template v-slot:label> <template v-slot:label>
{{ $t('schedules.cron.expression')}} {{ $t('schedules.cron.expression') }}
<b-link class="text-body" :id="'tooltip-' + index"> <b-link class="text-body" :id="'tooltip-' + index">
<help /> <help />
</b-link> </b-link>
<b-tooltip :target="'tooltip-' + index" placement="bottom"> <b-tooltip :target="'tooltip-' + index" placement="bottom">
<div v-if="isValid"> <div v-if="isValid">
<p class="font-weight-bold">3 Next occurences</p> <p class="font-weight-bold">
3 Next occurences
</p>
<span v-if="occurences.length"> <span v-if="occurences.length">
<span v-for="(occurence, x) in occurences" :key="x">{{occurence | date('LLL:ss')}}<br /></span> <span v-for="(occurence, x) in occurences" :key="x">{{ occurence | date('LLL:ss') }}<br></span>
</span> </span>
</div> </div>
<span v-else> <span v-else>
@@ -27,7 +32,7 @@
</b-tooltip> </b-tooltip>
</template> </template>
<b-form-input required :id="'input-cron-' + index" v-model="schedule.cron"></b-form-input> <b-form-input required :id="'input-cron-' + index" v-model="schedule.cron" />
<b-form-invalid-feedback> <b-form-invalid-feedback>
Enter at least 3 letters Enter at least 3 letters
@@ -37,95 +42,98 @@
</b-form-group> </b-form-group>
<b-form-group label-cols-sm="3" label-cols-lg="2" :label="$t('schedules.cron.backfilll')" <b-form-group
:label-for="'input-' + index"> label-cols-sm="3"
label-cols-lg="2"
:label="$t('schedules.cron.backfilll')"
:label-for="'input-' + index"
>
<date-picker <date-picker
v-model="backfillStart" v-model="backfillStart"
:required="false" :required="false"
type="datetime" type="datetime"
:id="'input-' + index" :id="'input-' + index"
></date-picker> />
</b-form-group> </b-form-group>
<b-form-group class="mb-0 text-right"> <b-form-group class="mb-0 text-right">
<b-btn variant="danger" @click="remove"> <b-btn variant="danger" @click="remove">
<delete/> <delete />
Delete Delete
</b-btn> </b-btn>
</b-form-group> </b-form-group>
</b-list-group-item> </b-list-group-item>
</template> </template>
<script> <script>
const cronstrue = require("cronstrue/i18n"); const cronstrue = require("cronstrue/i18n");
const cronParser = require("cron-parser"); const cronParser = require("cron-parser");
import Delete from "vue-material-design-icons/Delete"; import Delete from "vue-material-design-icons/Delete";
import Help from "vue-material-design-icons/HelpBox"; import Help from "vue-material-design-icons/HelpBox";
import DatePicker from "vue2-datepicker"; import DatePicker from "vue2-datepicker";
export default { export default {
components: { components: {
Delete, Delete,
Help, Help,
DatePicker DatePicker
},
props: {
schedule: {
type: Object,
required: true
}, },
index: { props: {
type: Number, schedule: {
required: true type: Object,
} 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) { index: {
let current = this.schedule; type: Number,
required: true
}
},
if (val) { computed: {
current.backfill = {"start": this.$moment(val).format()}; backfillStart: {
} else { get: function () {
delete current.backfill; 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) {
return occurences;
}
const interval = cronParser.parseExpression(this.schedule.cron);
for (let i = 0; i < 3; i++) {
occurences.push(interval.next().toDate());
} }
this.$emit("set", this.index, current);
}
},
occurences() {
const occurences = [];
if (!this.isValid) {
return occurences; return occurences;
} },
const interval = cronParser.parseExpression(this.schedule.cron); cronHumanReadable() {
for (let i = 0; i < 3; i++) { const locale = localStorage.getItem("lang") || "en";
occurences.push(interval.next().toDate()); try {
} return cronstrue.toString(this.schedule.cron, {locale});
return occurences; } catch {
}, return this.$t("schedules.cron.invalid");
cronHumanReadable() { }
const locale = localStorage.getItem("lang") || "en"; },
try { isValid() {
return cronstrue.toString(this.schedule.cron, { locale }); return require("cron-validator").isValidCron(this.schedule.cron);
} catch {
return this.$t("schedules.cron.invalid");
} }
}, },
isValid() { methods: {
return require("cron-validator").isValidCron(this.schedule.cron); remove() {
this.$emit("remove", this.index);
}
} }
}, };
methods: {
remove() {
this.$emit("remove", this.index);
}
}
};
</script> </script>

View File

@@ -10,26 +10,26 @@
</b-card> </b-card>
</template> </template>
<script> <script>
import YamlUtils from "../../utils/yamlUtils"; import YamlUtils from "../../utils/yamlUtils";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
computed: { computed: {
...mapState("graph", ["node"]), ...mapState("graph", ["node"]),
task() { task() {
return this.node && this.node.task; return this.node && this.node.task;
}, },
items() { items() {
const items = []; const items = [];
for (const property in this.task) { for (const property in this.task) {
const v = this.task[property]; const v = this.task[property];
const value = const value =
typeof v === "object" typeof v === "object"
? `<pre>${YamlUtils.stringify(v)}</pre>` ? `<pre>${YamlUtils.stringify(v)}</pre>`
: v; : v;
items.push({ property, value }); items.push({property, value});
}
return items;
} }
return items;
} }
} };
};
</script> </script>

View File

@@ -7,51 +7,50 @@
:text="name(trigger)" :text="name(trigger)"
button button
@click="showTriggerDetails(trigger)" @click="showTriggerDetails(trigger)"
> />
</b-avatar>
</span> </span>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
flow: { flow: {
type: Object, type: Object,
default: () => undefined, default: () => undefined,
},
execution: {
type: Object,
default: () => undefined,
},
}, },
execution: { components: {},
type: Object, methods: {
default: () => undefined, showTriggerDetails(trigger) {
}, this.$emit("showTriggerDetails", trigger);
}, },
components: {}, uid(trigger) {
methods: { return (this.flow ? this.flow.namespace + "-" + this.flow.id : this.execution.namespace + "-" + this.execution.flowId) + "-" + trigger.id
showTriggerDetails(trigger) { },
this.$emit("showTriggerDetails", trigger); name(trigger) {
}, let split = trigger.id.split(".");
uid(trigger) {
return (this.flow ? this.flow.namespace + "-" + this.flow.id : this.execution.namespace + "-" + this.execution.flowId) + '-' + trigger.id
},
name(trigger) {
let split = trigger.id.split(".");
return split[split.length - 1].substr(0, 1).toUpperCase(); return split[split.length - 1].substr(0, 1).toUpperCase();
},
}, },
}, computed: {
computed: { triggers() {
triggers() { if (this.flow && this.flow.triggers) {
if (this.flow && this.flow.triggers) { return this.flow.triggers
return this.flow.triggers } else if (this.execution && this.execution.trigger) {
} else if (this.execution && this.execution.trigger) { return [this.execution.trigger]
return [this.execution.trigger] } else {
} else { return []
return [] }
} }
} }
} };
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -11,25 +11,25 @@
</b-modal> </b-modal>
</template> </template>
<script> <script>
import Vars from "../executions/Vars"; import Vars from "../executions/Vars";
import Markdown from "../../utils/markdown"; import Markdown from "../../utils/markdown";
export default { export default {
components: { Vars }, components: {Vars},
props: { props: {
trigger: { trigger: {
type: Object, type: Object,
default: () => undefined default: () => undefined
},
}, },
}, computed: {
computed: { triggerData() {
triggerData() { if (this.trigger.description) {
if (this.trigger.description) { return {...this.trigger, description: Markdown.render(this.trigger.description)}
return {...this.trigger, description: Markdown.render(this.trigger.description)} }
}
return this.trigger return this.trigger
}
} }
} };
};
</script> </script>

View File

@@ -21,19 +21,19 @@
role="tabpanel" role="tabpanel"
> >
<b-card-body> <b-card-body>
<vars :data="trigger"/> <vars :data="trigger" />
</b-card-body> </b-card-body>
</b-collapse> </b-collapse>
</b-card> </b-card>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import Vars from '../executions/Vars' import Vars from "../executions/Vars"
export default { export default {
components: { Vars }, components: {Vars},
computed: { computed: {
...mapState("flow", ["flow"]), ...mapState("flow", ["flow"]),
} }
}; };
</script> </script>

View File

@@ -16,7 +16,9 @@
/> />
</b-breadcrumb> </b-breadcrumb>
<div> <div>
<b-tooltip placement="left" target="graph-orientation">{{$t('graph orientation')}}</b-tooltip> <b-tooltip placement="left" target="graph-orientation">
{{ $t('graph orientation') }}
</b-tooltip>
<b-btn size="sm" @click="toggleOrientation" id="graph-orientation"> <b-btn size="sm" @click="toggleOrientation" id="graph-orientation">
<arrow-collapse-down v-if="orientation" /> <arrow-collapse-down v-if="orientation" />
<arrow-collapse-right v-else /> <arrow-collapse-right v-else />
@@ -24,7 +26,7 @@
</div> </div>
</div> </div>
<div :class="{hide: !ready}" class="wrapper" ref="wrapper"></div> <div :class="{hide: !ready}" class="wrapper" ref="wrapper" />
<div class="hidden"> <div class="hidden">
<tree-node <tree-node
@onFilterGroup="onFilterGroup" @onFilterGroup="onFilterGroup"
@@ -32,13 +34,13 @@
v-for="node in filteredDataTree" v-for="node in filteredDataTree"
:key="slug(node)" :key="slug(node)"
:n="node" :n="node"
:isFlow="isFlow" :is-flow="isFlow"
/> />
<tree-node <tree-node
:ref="`node-${slug(virtualRootNode)}`" :ref="`node-${slug(virtualRootNode)}`"
v-if="virtualRootNode" v-if="virtualRootNode"
:n="virtualRootNode" :n="virtualRootNode"
:isFlow="isFlow" :is-flow="isFlow"
/> />
</div> </div>
@@ -48,290 +50,290 @@
</div> </div>
</template> </template>
<script> <script>
const dagreD3 = require("dagre-d3"); const dagreD3 = require("dagre-d3");
import TreeNode from "./TreeNode"; import TreeNode from "./TreeNode";
import * as d3 from "d3"; import * as d3 from "d3";
import ArrowCollapseRight from "vue-material-design-icons/ArrowCollapseRight"; import ArrowCollapseRight from "vue-material-design-icons/ArrowCollapseRight";
import ArrowCollapseDown from "vue-material-design-icons/ArrowCollapseDown"; import ArrowCollapseDown from "vue-material-design-icons/ArrowCollapseDown";
import VectorCircle from "vue-material-design-icons/Circle"; import VectorCircle from "vue-material-design-icons/Circle";
const parentHash = node => { const parentHash = node => {
if (node.parent) { if (node.parent) {
const parent = node.parent[0]; const parent = node.parent[0];
return nodeHash(parent) return nodeHash(parent)
} else { } else {
return undefined; return undefined;
}
};
const nodeHash = node => {
return (node.id + (node.value ? "-" + node.value : "")).hashCode();
} }
};
const nodeHash = node => {
return (node.id + (node.value ? "-" + node.value : "")).hashCode();
}
export default { export default {
components: { components: {
TreeNode, TreeNode,
ArrowCollapseDown, ArrowCollapseDown,
ArrowCollapseRight, ArrowCollapseRight,
VectorCircle VectorCircle
},
props: {
dataTree: {
type: Array,
required: true
}, },
label: { props: {
type: Function, dataTree: {
required: true type: Array,
required: true
},
label: {
type: Function,
required: true
},
isFlow: {
type: Boolean,
default: false
}
}, },
isFlow: { data() {
type: Boolean, return {
default: false ready: false,
} filterGroup: undefined,
}, orientation: true,
data() { zoom: undefined,
return { filteredDataTree: undefined,
ready: false, virtualRootNode: undefined
filterGroup: undefined, };
orientation: true,
zoom: undefined,
filteredDataTree: undefined,
virtualRootNode: undefined
};
},
watch: {
dataTree() {
this.generateGraph();
}, },
$route() { watch: {
if (this.$route.query.filter != this.filterGroup) { dataTree() {
this.filterGroup = this.$route.query.filter;
this.generateGraph(); this.generateGraph();
},
$route() {
if (this.$route.query.filter != this.filterGroup) {
this.filterGroup = this.$route.query.filter;
this.generateGraph();
}
} }
} },
}, created() {
created() { this.orientation = localStorage.getItem("topology-orientation") === "1";
this.orientation = localStorage.getItem("topology-orientation") === "1"; if (this.$route.query.filter) {
if (this.$route.query.filter) { this.filterGroup = this.$route.query.filter;
this.filterGroup = this.$route.query.filter; }
} },
}, mounted() {
mounted() {
this.generateGraph();
},
methods: {
toggleOrientation() {
this.orientation = !this.orientation;
localStorage.setItem(
"topology-orientation",
this.orientation ? 1 : 0
);
this.generateGraph(); this.generateGraph();
}, },
getVirtualRootNode() { methods: {
return this.filterGroup toggleOrientation() {
? { this.orientation = !this.orientation;
task: { localStorage.setItem(
id: this.filterGroup "topology-orientation",
} this.orientation ? 1 : 0
} );
: undefined; this.generateGraph();
}, },
generateGraph() { getVirtualRootNode() {
this.filteredDataTree = this.getFilteredDataTree(); return this.filterGroup
this.virtualRootNode = this.getVirtualRootNode(); ? {
// Create the input graph task: {
const arrowColor = "#ccc"; id: this.filterGroup
if (this.zoom) { }
this.zoom.on("zoom", null); }
} : undefined;
this.$refs.wrapper.innerHTML = },
'<svg id="svg-canvas" width="100%" style="min-height:800px"/>'; generateGraph() {
const g = new dagreD3.graphlib.Graph({ this.filteredDataTree = this.getFilteredDataTree();
compound: true, this.virtualRootNode = this.getVirtualRootNode();
multigraph: true // Create the input graph
}) const arrowColor = "#ccc";
.setGraph({}) if (this.zoom) {
.setDefaultEdgeLabel(function() { this.zoom.on("zoom", null);
return {}; }
}); this.$refs.wrapper.innerHTML =
"<svg id=\"svg-canvas\" width=\"100%\" style=\"min-height:800px\"/>";
const g = new dagreD3.graphlib.Graph({
compound: true,
multigraph: true
})
.setGraph({})
.setDefaultEdgeLabel(function() {
return {};
});
const getOptions = node => { const getOptions = node => {
const edgeOption = {}; const edgeOption = {};
if (node.relation !== "SEQUENTIAL") { if (node.relation !== "SEQUENTIAL") {
edgeOption.label = node.relation.toLowerCase(); edgeOption.label = node.relation.toLowerCase();
if (node.taskRun && node.taskRun.value) { if (node.taskRun && node.taskRun.value) {
edgeOption.label += ` : ${node.taskRun.value}`; edgeOption.label += ` : ${node.taskRun.value}`;
}
}
edgeOption.class =
{
ERROR: "error-edge",
DYNAMIC: "dynamic-edge",
CHOICE: "choice-edge",
PARALLEL: "choice-edge"
}[node.relation] || "";
return edgeOption;
};
const ancestorsHashes = new Set(
this.filteredDataTree.filter(e => e.parent).map(e => nodeHash(e.parent[0]))
);
const virtualRootId = this.getVirtualRootNode() && this.getVirtualRootNode().task.id
for (const node of this.filteredDataTree) {
const slug = this.slug(node);
const hash = parentHash(node);
g.setNode(slug, {
labelType: "html",
label: `<div class="node-binder" id="node-${slug}"/>`
});
const options = getOptions(node);
const parentId = node.parent && node.parent[0] && node.parent[0].id
if (ancestorsHashes.has(hash) && ["SEQUENTIAL", "ERROR"].includes(node.relation) && virtualRootId !== parentId) {
g.setEdge(parentHash(node), slug, options);
} else {
g.setEdge("parent node", slug, options)
} }
} }
edgeOption.class = const rootNode = {
{
ERROR: "error-edge",
DYNAMIC: "dynamic-edge",
CHOICE: "choice-edge",
PARALLEL: "choice-edge"
}[node.relation] || "";
return edgeOption;
};
const ancestorsHashes = new Set(
this.filteredDataTree.filter(e => e.parent).map(e => nodeHash(e.parent[0]))
);
const virtualRootId = this.getVirtualRootNode() && this.getVirtualRootNode().task.id
for (const node of this.filteredDataTree) {
const slug = this.slug(node);
const hash = parentHash(node);
g.setNode(slug, {
labelType: "html", labelType: "html",
label: `<div class="node-binder" id="node-${slug}"/>` clusterLabelPos: "bottom"
}); };
const options = getOptions(node); if (this.filterGroup) {
const parentId = node.parent && node.parent[0] && node.parent[0].id rootNode.label = `<div class="node-binder root-node-virtual" id="node-${this.slug(
if (ancestorsHashes.has(hash) && ['SEQUENTIAL', 'ERROR'].includes(node.relation) && virtualRootId !== parentId) { this.virtualRootNode
g.setEdge(parentHash(node), slug, options); )}"/>`;
} else { } else {
g.setEdge("parent node", slug, options) rootNode.class = "root-node";
rootNode.label = "<div class=\"vector-circle-wrapper\"/>";
rootNode.height = 30;
rootNode.width = 30;
} }
} g.setNode("parent node", rootNode);
const rootNode = { g.nodes().forEach(v => {
labelType: "html", const node = g.node(v);
clusterLabelPos: "bottom" if (node) {
}; node.paddingLeft = node.paddingRight = node.paddingTop = node.paddingBottom = 0;
if (this.filterGroup) { }
rootNode.label = `<div class="node-binder root-node-virtual" id="node-${this.slug( });
this.virtualRootNode if (!this.orientation) {
)}"/>`; g.graph().rankDir = "LR";
} else {
rootNode.class = "root-node";
rootNode.label = '<div class="vector-circle-wrapper"/>';
rootNode.height = 30;
rootNode.width = 30;
}
g.setNode("parent node", rootNode);
g.nodes().forEach(v => {
const node = g.node(v);
if (node) {
node.paddingLeft = node.paddingRight = node.paddingTop = node.paddingBottom = 0;
} }
}); const render = new dagreD3.render();
if (!this.orientation) { // Set up an SVG group so that we can translate the final graph.
g.graph().rankDir = "LR"; const svgWrapper = d3.select("#svg-canvas"),
} svgGroup = svgWrapper.append("g");
const render = new dagreD3.render(); // Run the renderer. This is what draws the final graph.
// Set up an SVG group so that we can translate the final graph. this.zoom = d3
const svgWrapper = d3.select("#svg-canvas"), .zoom()
svgGroup = svgWrapper.append("g"); .on("zoom", () => {
// Run the renderer. This is what draws the final graph. const t = d3.event.transform;
this.zoom = d3 svgGroup.attr(
.zoom() "transform",
.on("zoom", () => { `translate(${t.x},${t.y}) scale(${t.k})`
const t = d3.event.transform;
svgGroup.attr(
"transform",
`translate(${t.x},${t.y}) scale(${t.k})`
);
})
.scaleExtent([1, 1]);
svgWrapper.call(this.zoom);
svgWrapper.on("dblclick.zoom", null);
render(d3.select("#svg-canvas g"), g);
d3.selectAll("#svg-canvas g path").style("stroke", arrowColor);
d3.selectAll("#svg-canvas .edgePath marker").style(
"fill",
arrowColor
);
const transform = d3.zoomIdentity.translate(50, 50).translate(0, 0);
svgWrapper.call(this.zoom.transform, transform);
this.bindNodes();
},
virtalNodeReady() {
if (this.virtualRootNode) {
const vueNode = this.$refs[
`node-${this.slug(this.virtualRootNode)}`
];
return vueNode && vueNode.$el;
} else {
return true;
}
},
bindNodes() {
let ready = true;
for (const node of this.filteredDataTree) {
if (
!this.virtalNodeReady() ||
!this.$refs[this.slug(node)] ||
!this.$refs[this.slug(node)].length ||
!this.$refs["vector-circle"]
) {
ready = false;
}
}
if (ready) {
for (const node of this.filteredDataTree) {
this.$el
.querySelector(`#node-${this.slug(node)}`)
.appendChild(this.$refs[this.slug(node)][0].$el);
}
if (this.virtualRootNode) {
this.$el
.querySelector(
`#node-${this.slug(this.virtualRootNode)}`
)
.appendChild(
this.$refs[
`node-${this.slug(this.virtualRootNode)}`
].$el
); );
} else { })
this.$el .scaleExtent([1, 1]);
.querySelector(".vector-circle-wrapper") svgWrapper.call(this.zoom);
.appendChild(this.$refs["vector-circle"]); svgWrapper.on("dblclick.zoom", null);
}
this.ready = true; render(d3.select("#svg-canvas g"), g);
} else { d3.selectAll("#svg-canvas g path").style("stroke", arrowColor);
setTimeout(this.bindNodes, 30); d3.selectAll("#svg-canvas .edgePath marker").style(
} "fill",
}, arrowColor
onFilterGroup(group) {
if (this.$route.query.filter != group) {
this.filterGroup = group;
this.$router.push({
query: { ...this.$route.query, filter: group }
});
this.generateGraph();
}
},
slug(node) {
const hash =
node.task.id +
(node.taskRun && node.taskRun.value
? "-" + node.taskRun.value
: "");
return hash.hashCode();
},
getFilteredDataTree() {
if (this.filterGroup) {
return this.dataTree.filter(
node =>
node.groups &&
node.groups[node.groups.length - 1] === this.filterGroup
); );
} else { const transform = d3.zoomIdentity.translate(50, 50).translate(0, 0);
return this.dataTree.filter(node => !node.groups); svgWrapper.call(this.zoom.transform, transform);
this.bindNodes();
},
virtalNodeReady() {
if (this.virtualRootNode) {
const vueNode = this.$refs[
`node-${this.slug(this.virtualRootNode)}`
];
return vueNode && vueNode.$el;
} else {
return true;
}
},
bindNodes() {
let ready = true;
for (const node of this.filteredDataTree) {
if (
!this.virtalNodeReady() ||
!this.$refs[this.slug(node)] ||
!this.$refs[this.slug(node)].length ||
!this.$refs["vector-circle"]
) {
ready = false;
}
}
if (ready) {
for (const node of this.filteredDataTree) {
this.$el
.querySelector(`#node-${this.slug(node)}`)
.appendChild(this.$refs[this.slug(node)][0].$el);
}
if (this.virtualRootNode) {
this.$el
.querySelector(
`#node-${this.slug(this.virtualRootNode)}`
)
.appendChild(
this.$refs[
`node-${this.slug(this.virtualRootNode)}`
].$el
);
} else {
this.$el
.querySelector(".vector-circle-wrapper")
.appendChild(this.$refs["vector-circle"]);
}
this.ready = true;
} else {
setTimeout(this.bindNodes, 30);
}
},
onFilterGroup(group) {
if (this.$route.query.filter != group) {
this.filterGroup = group;
this.$router.push({
query: {...this.$route.query, filter: group}
});
this.generateGraph();
}
},
slug(node) {
const hash =
node.task.id +
(node.taskRun && node.taskRun.value
? "-" + node.taskRun.value
: "");
return hash.hashCode();
},
getFilteredDataTree() {
if (this.filterGroup) {
return this.dataTree.filter(
node =>
node.groups &&
node.groups[node.groups.length - 1] === this.filterGroup
);
} else {
return this.dataTree.filter(node => !node.groups);
}
} }
},
computed: {
groups() {
const groups = new Set();
this.dataTree.forEach(node =>
(node.groups || []).forEach(group => groups.add(group))
);
return groups;
}
},
destroyed() {
this.ready = false;
} }
}, };
computed: {
groups() {
const groups = new Set();
this.dataTree.forEach(node =>
(node.groups || []).forEach(group => groups.add(group))
);
return groups;
}
},
destroyed() {
this.ready = false;
}
};
</script> </script>
<style lang="scss"> <style lang="scss">
@import "../../styles/variable"; @import "../../styles/variable";

View File

@@ -1,15 +1,15 @@
<template> <template>
<div class="node-wrapper"> <div class="node-wrapper">
<div class="status-color" v-if="n.taskRun" :class="contentCls"></div> <div class="status-color" v-if="n.taskRun" :class="contentCls" />
<div class="task-content"> <div class="task-content">
<div class="card-header"> <div class="card-header">
<div class="icon-wrapper"> <div class="icon-wrapper">
<!-- <img src=""/> --> <!-- <img src=""/> -->
</div> </div>
<div class="task-title"> <div class="task-title">
<span>{{task.id | ellipsis(18)}}</span> <span>{{ task.id | ellipsis(18) }}</span>
</div> </div>
<!-- <menu-open class="node-action" @click="onSettings" /> --> <!-- <menu-open class="node-action" @click="onSettings" /> -->
</div> </div>
<div v-if="task.state" class="status-wrapper"> <div v-if="task.state" class="status-wrapper">
<status :status="state" /> <status :status="state" />
@@ -82,104 +82,104 @@
</div> </div>
<b-modal size="xl" :hide-footer="true" :id="`modal-${hash}`" :title="`Task ${task.id}`"> <b-modal size="xl" :hide-footer="true" :id="`modal-${hash}`" :title="`Task ${task.id}`">
<pre>{{taskYaml}}</pre> <pre>{{ taskYaml }}</pre>
</b-modal> </b-modal>
</div> </div>
</template> </template>
<script> <script>
import Console from "vue-material-design-icons/Console"; import Console from "vue-material-design-icons/Console";
import Graph from "vue-material-design-icons/Graph"; import Graph from "vue-material-design-icons/Graph";
import CodeTags from "vue-material-design-icons/CodeTags"; import CodeTags from "vue-material-design-icons/CodeTags";
import FormatListChecks from "vue-material-design-icons/FormatListChecks"; import FormatListChecks from "vue-material-design-icons/FormatListChecks";
import LocationExit from "vue-material-design-icons/LocationExit"; import LocationExit from "vue-material-design-icons/LocationExit";
import CurrentAc from "vue-material-design-icons/CurrentAc"; import CurrentAc from "vue-material-design-icons/CurrentAc";
import { mapState } from "vuex"; import {mapState} from "vuex";
import Status from "../Status"; import Status from "../Status";
import md5 from "md5"; import md5 from "md5";
import YamlUtils from "../../utils/yamlUtils"; import YamlUtils from "../../utils/yamlUtils";
import MarkdownTooltip from "../../components/layout/MarkdownTooltip"; import MarkdownTooltip from "../../components/layout/MarkdownTooltip";
export default { export default {
components: { components: {
MarkdownTooltip, MarkdownTooltip,
Status, Status,
Console, Console,
Graph, Graph,
CodeTags, CodeTags,
FormatListChecks, FormatListChecks,
LocationExit, LocationExit,
CurrentAc CurrentAc
},
props: {
n: {
type: Object,
default: undefined
}, },
isFlow: { props: {
type: Boolean, n: {
default: false type: Object,
} default: undefined
}, },
methods: { isFlow: {
taskRunOutputToken(taskRun) { type: Boolean,
return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: '')); default: false
},
onFilterGroup() {
this.$emit("onFilterGroup", this.task.id);
},
onSettings() {
if (this.node) {
this.$store.dispatch("graph/setNode", undefined);
} else {
this.$store.dispatch("graph/setNode", this.n);
this.$emit("onSettings");
} }
} },
}, methods: {
computed: { taskRunOutputToken(taskRun) {
...mapState("graph", ["node"]), return md5(taskRun.taskId + (taskRun.value ? ` - ${taskRun.value}`: ""));
hasLogs() { },
// @TODO onFilterGroup() {
return true; this.$emit("onFilterGroup", this.task.id);
},
onSettings() {
if (this.node) {
this.$store.dispatch("graph/setNode", undefined);
} else {
this.$store.dispatch("graph/setNode", this.n);
this.$emit("onSettings");
}
}
},
computed: {
...mapState("graph", ["node"]),
hasLogs() {
// @TODO
return true;
// return ( // return (
// this.attempts.filter(attempt => attempt.logs.length).length > 0 // this.attempts.filter(attempt => attempt.logs.length).length > 0
// ); // );
}, },
hasOutputs() { hasOutputs() {
return this.n.taskRun && this.n.taskRun.outputs; return this.n.taskRun && this.n.taskRun.outputs;
}, },
attempts() { attempts() {
return this.n.taskRun && this.n.taskRun.attempts return this.n.taskRun && this.n.taskRun.attempts
? this.n.taskRun.attempts ? this.n.taskRun.attempts
: []; : [];
}, },
hash() { hash() {
return this.task.id.hashCode(); return this.task.id.hashCode();
}, },
childrenCount() { childrenCount() {
return this.task.tasks ? this.task.tasks.length : 0; return this.task.tasks ? this.task.tasks.length : 0;
}, },
taskYaml() { taskYaml() {
return YamlUtils.stringify(this.n); return YamlUtils.stringify(this.n);
}, },
state() { state() {
return this.n.taskRun ? this.n.taskRun.state.current : "SUCCESS"; return this.n.taskRun ? this.n.taskRun.state.current : "SUCCESS";
}, },
contentCls() { contentCls() {
return { return {
"is-success": !["RUNNING", "FAILED"].includes(this.state), "is-success": !["RUNNING", "FAILED"].includes(this.state),
"is-running": this.state === "RUNNING", "is-running": this.state === "RUNNING",
"is-failed": this.state === "FAILED" "is-failed": this.state === "FAILED"
}; };
}, },
task() { task() {
return this.n.task; return this.n.task;
}, },
value () { value () {
return this.n.taskRun && this.n.taskRun.value return this.n.taskRun && this.n.taskRun.value
}
} }
} };
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../styles/_variable.scss"; @import "../../styles/_variable.scss";

View File

@@ -3,11 +3,15 @@
<b-navbar type="dark" variant="dark"> <b-navbar type="dark" variant="dark">
<b-btn-group> <b-btn-group>
<b-button @click="autoFold(true)" size="sm" variant="light" v-b-tooltip.hover.top="$t('Fold content lines')"> <b-button @click="autoFold(true)" size="sm" variant="light" v-b-tooltip.hover.top="$t('Fold content lines')">
<unfold-less-horizontal/> <unfold-less-horizontal />
</b-button> </b-button>
<b-button @click="unfoldAll" size="sm" variant="light" <b-button
v-b-tooltip.hover.top="$t('Unfold content lines')"> @click="unfoldAll"
<unfold-more-horizontal/> size="sm"
variant="light"
v-b-tooltip.hover.top="$t('Unfold content lines')"
>
<unfold-more-horizontal />
</b-button> </b-button>
</b-btn-group> </b-btn-group>
</b-navbar> </b-navbar>
@@ -19,9 +23,9 @@
:lang="lang" :lang="lang"
theme="merbivore_soft" theme="merbivore_soft"
:width="width" :width="width"
minLines="5" min-lines="5"
:height="height" :height="height"
></editor> />
</div> </div>
</template> </template>
@@ -29,7 +33,7 @@
import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal"; import UnfoldLessHorizontal from "vue-material-design-icons/UnfoldLessHorizontal";
import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal"; import UnfoldMoreHorizontal from "vue-material-design-icons/UnfoldMoreHorizontal";
import YamlUtils from '../../utils/yamlUtils'; import YamlUtils from "../../utils/yamlUtils";
export default { export default {
props: { props: {
@@ -60,7 +64,7 @@
editor.setOptions({ editor.setOptions({
minLines: 5, minLines: 5,
maxLines: Infinity, maxLines: Infinity,
fontFamily: '"Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', fontFamily: "\"Source Code Pro\", SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace",
showPrintMargin: false, showPrintMargin: false,
tabSize: 2, tabSize: 2,
wrap: false, wrap: false,
@@ -73,15 +77,15 @@
name: "save", name: "save",
bindKey: {win: "Ctrl-S", mac: "Cmd-S"}, bindKey: {win: "Ctrl-S", mac: "Cmd-S"},
exec: (editor) => { exec: (editor) => {
this.$emit('onSave', editor.session.getValue()) this.$emit("onSave", editor.session.getValue())
}, },
}); });
setTimeout(() => { setTimeout(() => {
this.autoFold(localStorage.getItem('autofoldTextEditor') === "1") this.autoFold(localStorage.getItem("autofoldTextEditor") === "1")
}) })
}, },
trimContent(text) { trimContent(text) {
return text.split('\n').map(line => line.trim()).join('\n') return text.split("\n").map(line => line.trim()).join("\n")
}, },
autoFold(autoFold) { autoFold(autoFold) {
//we may add try in case content is not serializable a json //we may add try in case content is not serializable a json
@@ -92,8 +96,8 @@
for (const foldableToken of foldableTokens) { for (const foldableToken of foldableTokens) {
const search = this.trimContent(foldableToken) const search = this.trimContent(foldableToken)
const index = trimmedContent.indexOf(search) const index = trimmedContent.indexOf(search)
const line = trimmedContent.slice(0, index).split('\n').length + lineDiff const line = trimmedContent.slice(0, index).split("\n").length + lineDiff
lineDiff = line + search.split('\n').length - 2 lineDiff = line + search.split("\n").length - 2
trimmedContent = trimmedContent.slice(index + search.length) trimmedContent = trimmedContent.slice(index + search.length)
this.ed.getSession().$toggleFoldWidget(line - 2, {}) this.ed.getSession().$toggleFoldWidget(line - 2, {})
} }
@@ -103,12 +107,12 @@
for (const n of node) { for (const n of node) {
this.getFoldLines(n, foldableTokens, autoFold) this.getFoldLines(n, foldableTokens, autoFold)
} }
} else if (typeof (node) === 'object') { } else if (typeof (node) === "object") {
for (const key in node) { for (const key in node) {
this.getFoldLines(node[key], foldableTokens, autoFold) this.getFoldLines(node[key], foldableTokens, autoFold)
} }
} else if (typeof (node) === 'string') { } else if (typeof (node) === "string") {
if (node.split('\n').length > 1 && autoFold) { if (node.split("\n").length > 1 && autoFold) {
foldableTokens.push(node) foldableTokens.push(node)
} }
} }
@@ -117,7 +121,7 @@
this.ed.getSession().expandFolds(this.ed.getSession().getAllFolds()) this.ed.getSession().expandFolds(this.ed.getSession().getAllFolds())
}, },
onInput(value) { onInput(value) {
this.$emit('input', value); this.$emit("input", value);
} }
}, },
}; };

View File

@@ -1,6 +1,6 @@
<template> <template>
<b-navbar class="bottom-line" fixed="bottom" toggleable="lg"> <b-navbar class="bottom-line" fixed="bottom" toggleable="lg">
<slot/> <slot />
</b-navbar> </b-navbar>
</template> </template>
<script> <script>

View File

@@ -1,40 +1,39 @@
<template> <template>
<div> <div>
<b-navbar toggleable="lg" type="light" variant="light" v-if="hasNavBar"> <b-navbar toggleable="lg" type="light" variant="light" v-if="hasNavBar">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle> <b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-nav-form> <b-nav-form>
<slot name="navbar"></slot> <slot name="navbar" />
</b-nav-form> </b-nav-form>
</b-collapse> </b-collapse>
</b-navbar> </b-navbar>
<slot name="top"></slot> <slot name="top" />
<slot name="table"></slot> <slot name="table" />
<pagination :total="total" :max="max" @onPageChanged="onPageChanged" /> <pagination :total="total" :max="max" @onPageChanged="onPageChanged" />
</div> </div>
</template> </template>
<script> <script>
import Pagination from "./Pagination"; import Pagination from "./Pagination";
export default { export default {
components: { Pagination }, components: {Pagination},
computed: { computed: {
hasNavBar() { hasNavBar() {
return !!this.$slots["navbar"]; return !!this.$slots["navbar"];
},
}, },
}, props: {
props: { total: {type: Number, required: true},
total: { type: Number, required: true }, max: {type: Number, required:false, default: undefined}},
max: {type: Number, required:false} methods: {
}, onPageChanged(pagination) {
methods: { this.$emit("onPageChanged", pagination);
onPageChanged(pagination) { },
this.$emit("onPageChanged", pagination);
}, },
}, };
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -7,7 +7,7 @@
type="datetime" type="datetime"
class="sm" class="sm"
:placeholder="$t('start datetime')" :placeholder="$t('start datetime')"
></date-picker> />
<date-picker <date-picker
@input="onDate" @input="onDate"
v-model="end" v-model="end"
@@ -15,43 +15,43 @@
type="datetime" type="datetime"
class="sm" class="sm"
:placeholder="$t('end datetime')" :placeholder="$t('end datetime')"
></date-picker> />
</div> </div>
</template> </template>
<script> <script>
import DatePicker from "vue2-datepicker"; import DatePicker from "vue2-datepicker";
export default { export default {
components: { DatePicker }, components: {DatePicker},
data() { data() {
return { return {
start: null, start: null,
end: null end: null
};
},
created() {
if (this.$route.query.start) {
this.start = new Date(parseInt(this.$route.query.start));
}
if (this.$route.query.end) {
this.end = new Date(parseInt(this.$route.query.end));
}
},
methods: {
onDate() {
const start = this.start,
end = this.end;
const dateRange = {
start: start ? start.toISOString() : null,
end: end ? end.toISOString() : null
}; };
const query = { ...this.$route.query }; },
query.start = start ? start.getTime() : undefined; created() {
query.end = end ? end.getTime() : undefined; if (this.$route.query.start) {
this.$router.push({ query }); this.start = new Date(parseInt(this.$route.query.start));
this.$emit("onDate", dateRange); }
if (this.$route.query.end) {
this.end = new Date(parseInt(this.$route.query.end));
}
},
methods: {
onDate() {
const start = this.start,
end = this.end;
const dateRange = {
start: start ? start.toISOString() : null,
end: end ? end.toISOString() : null
};
const query = {...this.$route.query};
query.start = start ? start.getTime() : undefined;
query.end = end ? end.getTime() : undefined;
this.$router.push({query});
this.$emit("onDate", dateRange);
}
} }
} };
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.time-line { .time-line {

View File

@@ -1,36 +1,38 @@
<template> <template>
<div> <div>
<span v-html="markdownRenderer"></span> <span v-html="markdownRenderer" />
</div> </div>
</template> </template>
<script> <script>
import Prism from "prismjs"; import Prism from "prismjs";
import "prismjs/themes/prism-okaidia.css"; import "prismjs/themes/prism-okaidia.css";
import 'prismjs/components/prism-yaml.min'; import "prismjs/components/prism-yaml.min";
import Markdown from "../../utils/markdown"; import Markdown from "../../utils/markdown";
export default { export default {
props: { props: {
watches: { watches: {
type: Array, type: Array,
default: () => ['source', 'show', 'toc'], default: () => ["source", "show", "toc"],
}, },
source: { source: {
type: String, type: String,
default: ``, default: "",
}, },
permalink: { permalink: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
}, },
computed: { computed: {
markdownRenderer() { markdownRenderer() {
const outHtml = Markdown.render(this.source, {permalink: this.permalink}); const outHtml = Markdown.render(this.source, {
permalink: this.permalink,
});
this.$emit('rendered', outHtml) this.$emit("rendered", outHtml);
this.$nextTick(() => { this.$nextTick(() => {
Prism.highlightAll(); Prism.highlightAll();
@@ -39,7 +41,6 @@
return outHtml; return outHtml;
}, },
}, },
}; };
</script> </script>
@@ -52,5 +53,4 @@
font-weight: normal; font-weight: normal;
} }
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<span v-if="description"> <span v-if="description">
<help-circle title="" :id="'tooltip-desc-' + id" /> <help-circle title="" :id="'tooltip-desc-' + id" />
<b-popover triggers="hover" :target="'tooltip-desc-' + id" placement="bottom"> <b-popover triggers="hover" :target="'tooltip-desc-' + id" placement="bottom">
<markdown :source="description"></markdown> <markdown :source="description" />
</b-popover> </b-popover>
</span> </span>
</template> </template>
@@ -22,7 +22,7 @@
}, },
description: { description: {
type: String, type: String,
default: ``, default: "",
} }
}, },
computed: { computed: {

View File

@@ -6,7 +6,7 @@
@change="pageSizeChange" @change="pageSizeChange"
size="sm" size="sm"
:options="pageOptions" :options="pageOptions"
></b-form-select> />
</div> </div>
<div> <div>
<b-pagination <b-pagination
@@ -18,49 +18,50 @@
size="sm" size="sm"
class="my-0" class="my-0"
align="right" align="right"
></b-pagination> />
</div> </div>
<small v-if="max" class="btn btn-sm btn-outline-light text-muted"
>{{ $t('Max displayed') }}: {{ max }}</small <small v-if="max" class="btn btn-sm btn-outline-light text-muted">
> {{ $t('Max displayed') }}: {{ max }}
<small class="btn btn-sm btn-outline-light text-muted" </small>
>{{ $t('Total') }}: {{ total }}</small
> <small class="btn btn-sm btn-outline-light text-muted">
{{ $t('Total') }}: {{ total }}
</small>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
total: { type: Number, required: true }, total: {type: Number, required: true},
max: {type: Number, required:false} max: {type: Number, required:false, default: undefined}},
}, data() {
data() { return {
return { size: parseInt(this.$route.query.size || 25),
size: parseInt(this.$route.query.size || 25), page: parseInt(this.$route.query.page || 1),
page: parseInt(this.$route.query.page || 1), pageOptions: [
pageOptions: [ {value: 10, text: `10 ${this.$t("Per page")}`},
{ value: 10, text: `10 ${this.$t("Per page")}` }, {value: 25, text: `25 ${this.$t("Per page")}`},
{ value: 25, text: `25 ${this.$t("Per page")}` }, {value: 50, text: `50 ${this.$t("Per page")}`},
{ value: 50, text: `50 ${this.$t("Per page")}` }, {value: 100, text: `100 ${this.$t("Per page")}`},
{ value: 100, text: `100 ${this.$t("Per page")}` }, ],
], };
};
},
methods: {
pageSizeChange() {
this.$emit("onPageChanged", {
page: 1,
size: this.size,
});
}, },
pageChanged(page) { methods: {
this.$emit("onPageChanged", { pageSizeChange() {
page: page, this.$emit("onPageChanged", {
size: this.size, page: 1,
}); size: this.size,
});
},
pageChanged(page) {
this.$emit("onPageChanged", {
page: page,
size: this.size,
});
},
}, },
}, };
};
</script> </script>
<style scoped> <style scoped>
select { select {

View File

@@ -2,56 +2,60 @@
<b-button-group size="sm"> <b-button-group size="sm">
<b-tooltip <b-tooltip
target="toggle-automatic-refresh-action" target="toggle-automatic-refresh-action"
>{{$t('toggle periodic refresh each 10 seconds')}}</b-tooltip> >
{{ $t('toggle periodic refresh each 10 seconds') }}
</b-tooltip>
<b-button @click="toggleAutoRefresh" :pressed="autoRefresh" id="toggle-automatic-refresh-action"> <b-button @click="toggleAutoRefresh" :pressed="autoRefresh" id="toggle-automatic-refresh-action">
<clock/> <span class="label">{{$t('automatic refresh')}}</span> <clock /> <span class="label">{{ $t('automatic refresh') }}</span>
</b-button> </b-button>
<b-tooltip target="trigger-refresh-action">{{ $t('trigger refresh') }}</b-tooltip> <b-tooltip target="trigger-refresh-action">
{{ $t('trigger refresh') }}
</b-tooltip>
<b-button @click="triggerRefresh" id="trigger-refresh-action"> <b-button @click="triggerRefresh" id="trigger-refresh-action">
<refresh/> <span class="label">{{ $t('trigger refresh') }}</span> <refresh /> <span class="label">{{ $t('trigger refresh') }}</span>
</b-button> </b-button>
</b-button-group> </b-button-group>
</template> </template>
<script> <script>
import Refresh from "vue-material-design-icons/Refresh"; import Refresh from "vue-material-design-icons/Refresh";
import Clock from "vue-material-design-icons/Clock"; import Clock from "vue-material-design-icons/Clock";
export default { export default {
components: { Refresh, Clock }, components: {Refresh, Clock},
data() { data() {
return { return {
autoRefresh: false, autoRefresh: false,
refreshHandler: undefined refreshHandler: undefined
}; };
}, },
created() { created() {
this.autoRefresh = localStorage.getItem("autoRefresh") === "1"; this.autoRefresh = localStorage.getItem("autoRefresh") === "1";
}, },
methods: { methods: {
toggleAutoRefresh() { toggleAutoRefresh() {
this.autoRefresh = !this.autoRefresh; this.autoRefresh = !this.autoRefresh;
localStorage.setItem("autoRefresh", this.autoRefresh ? "1" : "0"); localStorage.setItem("autoRefresh", this.autoRefresh ? "1" : "0");
if (this.autoRefresh) { if (this.autoRefresh) {
this.refreshHandler = setInterval(this.triggerRefresh, 10000); this.refreshHandler = setInterval(this.triggerRefresh, 10000);
this.triggerRefresh() this.triggerRefresh()
} else { } else {
this.stopRefresh(); this.stopRefresh();
}
},
triggerRefresh() {
this.$emit("onRefresh");
},
stopRefresh() {
if (this.refreshHandler) {
clearInterval(this.refreshHandler);
this.refreshHandler = undefined
}
} }
}, },
triggerRefresh() { beforeDestroy() {
this.$emit("onRefresh"); this.stopRefresh();
},
stopRefresh() {
if (this.refreshHandler) {
clearInterval(this.refreshHandler);
this.refreshHandler = undefined
}
} }
}, };
beforeDestroy() {
this.stopRefresh();
}
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -6,37 +6,37 @@
@input="onSearch" @input="onSearch"
v-model="search" v-model="search"
:placeholder="$t('search')" :placeholder="$t('search')"
></b-form-input> />
</b-nav-form> </b-nav-form>
</template> </template>
<script> <script>
import { debounce } from "throttle-debounce"; import {debounce} from "throttle-debounce";
export default { export default {
created() { created() {
if (this.$route.query.q) { if (this.$route.query.q) {
this.search = this.$route.query.q; this.search = this.$route.query.q;
}
this.searchDebounce = debounce(300, () => {
this.$emit("onSearch", this.search);
});
},
data() {
return {
search: ""
};
},
methods: {
onSearch() {
const query = { ...this.$route.query, q: this.search, page: 1 };
if (!this.search) {
delete query.q;
} }
this.$router.push({ query }); this.searchDebounce = debounce(300, () => {
this.searchDebounce(); this.$emit("onSearch", this.search);
});
}, },
}, data() {
destroyed() { return {
this.searchDebounce.cancel(); search: ""
} };
}; },
methods: {
onSearch() {
const query = {...this.$route.query, q: this.search, page: 1};
if (!this.search) {
delete query.q;
}
this.$router.push({query});
this.searchDebounce();
},
},
destroyed() {
this.searchDebounce.cancel();
}
};
</script> </script>

View File

@@ -1,27 +1,27 @@
<template> <template>
<b-form-select @input="searchStatus" v-model="selected" size="sm" :options="statuses"></b-form-select> <b-form-select @input="searchStatus" v-model="selected" size="sm" :options="statuses" />
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
statuses: ["all", "running", "success", "failed"], statuses: ["all", "running", "success", "failed"],
selected: "all" selected: "all"
}; };
}, },
created() { created() {
if (this.$route.query.status) { if (this.$route.query.status) {
this.selected = this.$route.query.status.toLowerCase(); this.selected = this.$route.query.status.toLowerCase();
} }
}, },
methods: { methods: {
searchStatus() { searchStatus() {
const status = this.selected.toUpperCase(); const status = this.selected.toUpperCase();
if (this.$route.query.status !== status) { if (this.$route.query.status !== status) {
this.$router.push({ query: { ...this.$route.query, status } }); this.$router.push({query: {...this.$route.query, status}});
this.$emit("onRefresh"); this.$emit("onRefresh");
}
} }
} }
} };
};
</script> </script>

View File

@@ -1,11 +1,15 @@
<template> <template>
<b-navbar v-if="topNavbar" :class="menuCollapsed" class="top-line" type="dark" variant="dark"> <b-navbar v-if="topNavbar" :class="menuCollapsed" class="top-line" type="dark" variant="dark">
<b-navbar-nav> <b-navbar-nav>
<b-nav-text > <b-nav-text>
<h1>{{title}}</h1> <h1>{{ title }}</h1>
<b-breadcrumb> <b-breadcrumb>
<b-breadcrumb-item><router-link :to="{ name: 'home'}"><home /> {{$t('home')}}</router-link></b-breadcrumb-item> <b-breadcrumb-item>
<router-link :to="{name: 'home'}">
<home /> {{ $t('home') }}
</router-link>
</b-breadcrumb-item>
<b-breadcrumb-item v-for="(item, x) in topNavbar.breadcrumb" :to="item.link" :text="item.label" :key="x" /> <b-breadcrumb-item v-for="(item, x) in topNavbar.breadcrumb" :to="item.link" :text="item.label" :key="x" />
</b-breadcrumb> </b-breadcrumb>
</b-nav-text> </b-nav-text>
@@ -14,31 +18,31 @@
</b-navbar> </b-navbar>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import Home from "vue-material-design-icons/Home"; import Home from "vue-material-design-icons/Home";
import Auth from "Override/components/auth/Auth"; import Auth from "Override/components/auth/Auth";
export default { export default {
components: { components: {
Home, Home,
Auth, Auth,
},
props: {
menuCollapsed : {
type: String,
required: true
}
},
computed: {
...mapState("layout", ["topNavbar"]),
title() {
return this.topNavbar.title;
}, },
breadcrumb() { props: {
return this.topNavbar.breadcrumb.join(" > "); menuCollapsed : {
type: String,
required: true
}
},
computed: {
...mapState("layout", ["topNavbar"]),
title() {
return this.topNavbar.title;
},
breadcrumb() {
return this.topNavbar.breadcrumb.join(" > ");
}
} }
} };
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/variable"; @import "../../styles/variable";

View File

@@ -12,7 +12,7 @@
size="sm" size="sm"
@input="onChange" @input="onChange"
:placeholder="$t('search') + '...'" :placeholder="$t('search') + '...'"
></b-form-input> />
</b-form-group> </b-form-group>
</b-col> </b-col>
<b-col md="6"> <b-col md="6">
@@ -26,20 +26,20 @@
</b-row> </b-row>
</template> </template>
<script> <script>
import LogLevelSelector from "./LogLevelSelector"; import LogLevelSelector from "./LogLevelSelector";
export default { export default {
components: { LogLevelSelector }, components: {LogLevelSelector},
data() { data() {
return { return {
filter: "", filter: "",
}; };
},
methods: {
onChange() {
const query = { ...this.$route.query, q: this.filter, page: 1 };
this.$router.push({ query });
this.$emit("onChange");
}, },
}, methods: {
}; onChange() {
const query = {...this.$route.query, q: this.filter, page: 1};
this.$router.push({query});
this.$emit("onChange");
},
},
};
</script> </script>

View File

@@ -6,29 +6,29 @@
size="sm" size="sm"
@input="onChange" @input="onChange"
:options="levelOptions" :options="levelOptions"
></b-form-select> />
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
level: "INFO", level: "INFO",
levelOptions: [ levelOptions: [
"TRACE", "TRACE",
"DEBUG", "DEBUG",
"INFO", "INFO",
"WARN", "WARN",
"ERROR", "ERROR",
"CRITICAL", "CRITICAL",
], ],
}; };
},
methods: {
onChange() {
const query = { ...this.$route.query, level: this.level };
this.$router.push({ query });
this.$emit("onChange");
}, },
}, methods: {
}; onChange() {
const query = {...this.$route.query, level: this.level};
this.$router.push({query});
this.$emit("onChange");
},
},
};
</script> </script>

View File

@@ -16,60 +16,61 @@
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
log: { log: {
type: Object, type: Object,
required: true, required: true,
},
filter: {
type: String,
default: "",
},
level: {
type: String,
required: true,
},
excludeMetas: {
type: Array,
default: () => [],
},
}, },
filter: { computed: {
type: String, metaWithValue() {
default: "", const metaWithValue = [];
}, const excludes = [
level: { "message",
type: String, "timestamp",
}, "thread",
excludeMetas: { "taskRunId",
type: Array, "level",
default: () => [], ];
}, excludes.push.apply(excludes, this.excludeMetas);
}, for (const key in this.log) {
computed: { if (this.log[key] && !excludes.includes(key)) {
metaWithValue() { metaWithValue.push({key, value: this.log[key]});
const metaWithValue = []; }
const excludes = [
"message",
"timestamp",
"thread",
"taskRunId",
"level",
];
excludes.push.apply(excludes, this.excludeMetas);
for (const key in this.log) {
if (this.log[key] && !excludes.includes(key)) {
metaWithValue.push({ key, value: this.log[key] });
} }
} return metaWithValue;
return metaWithValue; },
levelClass() {
return {
TRACE: "badge-info",
DEBUG: "badge-secondary",
INFO: "badge-primary",
WARN: "badge-warning",
ERROR: "badge-danger",
CRITICAL: "badge-danger font-weight-bold",
}[this.log.level];
},
filtered() {
return (
this.log.message &&
this.log.message.toLowerCase().includes(this.filter)
);
},
}, },
levelClass() { };
return {
TRACE: "badge-info",
DEBUG: "badge-secondary",
INFO: "badge-primary",
WARN: "badge-warning",
ERROR: "badge-danger",
CRITICAL: "badge-danger font-weight-bold",
}[this.log.level];
},
filtered() {
return (
this.log.message &&
this.log.message.toLowerCase().includes(this.filter)
);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../styles/_variable.scss"; @import "../../styles/_variable.scss";

View File

@@ -1,7 +1,9 @@
<template> <template>
<div v-if="execution" class="log-wrapper text-white"> <div v-if="execution" class="log-wrapper text-white">
<div v-for="taskItem in execution.taskRunList" :key="taskItem.id"> <div v-for="taskItem in execution.taskRunList" :key="taskItem.id">
<template v-if="(!task || task.id === taskItem.id) && taskItem.attempts"> <template
v-if="(!task || task.id === taskItem.id) && taskItem.attempts"
>
<div class="bg-dark attempt-wrapper"> <div class="bg-dark attempt-wrapper">
<template v-for="(attempt, index) in taskItem.attempts"> <template v-for="(attempt, index) in taskItem.attempts">
<div <div
@@ -13,41 +15,55 @@
:target="`attempt-${index}-${taskItem.id}`" :target="`attempt-${index}-${taskItem.id}`"
triggers="hover" triggers="hover"
> >
{{ $t('from') }} : {{ attempt.state.startDate | date('LLL:ss') }} {{ $t("from") }} :
<br/> {{ attempt.state.startDate | date("LLL:ss") }}
{{ $t('to') }} : {{ attempt.state.endDate | date('LLL:ss') }} <br>
<br/> {{ $t("to") }} :
<br/> {{ attempt.state.endDate | date("LLL:ss") }}
<clock/> <br>
{{ $t('duration') }} : {{ attempt.state.duration | humanizeDuration }} <br>
<clock />
{{ $t("duration") }} :
{{ attempt.state.duration | humanizeDuration }}
</b-tooltip> </b-tooltip>
<div class="attempt-header"> <div class="attempt-header">
<div class="attempt-number mr-1"> <div class="attempt-number mr-1">
{{ $t('attempt') }} {{ index + 1 }} {{ $t("attempt") }} {{ index + 1 }}
</div> </div>
<div class="task-id flex-grow-1"> <div class="task-id flex-grow-1">
<code>{{ taskItem.taskId }}</code> <code>{{ taskItem.taskId }}</code>
<small v-if="taskItem.value"> {{ taskItem.value }}</small> <small v-if="taskItem.value">
{{ taskItem.value }}</small>
</div> </div>
<b-button-group> <b-button-group>
<b-button <b-button
v-if="taskItem.outputs" v-if="taskItem.outputs"
:title="$t('toggle metrics')" :title="$t('toggle metrics')"
@click="toggleShowMetric(taskItem, index)" @click="
><chart-areaspline :title="$t('toggle metrics')" /></b-button> toggleShowMetric(taskItem, index)
"
>
<chart-areaspline
:title="$t('toggle metrics')"
/>
</b-button>
<b-button <b-button
v-if="taskItem.outputs" v-if="taskItem.outputs"
:title="$t('toggle output')" :title="$t('toggle output')"
@click="toggleShowOutput(taskItem)" @click="toggleShowOutput(taskItem)"
><location-exit :title="$t('toggle output')" /></b-button> >
<location-exit
:title="$t('toggle output')"
/>
</b-button>
<restart <restart
:key="`restart-${index}-${attempt.state.startDate}`" :key="`restart-${index}-${attempt.state.startDate}`"
:isButtonGroup="true" :is-button-group="true"
:execution="execution" :execution="execution"
:task="taskItem" :task="taskItem"
/> />
@@ -57,7 +73,9 @@
<!-- Log lines --> <!-- Log lines -->
<template> <template>
<template v-for="(log, i) in findLogs(taskItem.id, index)"> <template
v-for="(log, i) in findLogs(taskItem.id, index)"
>
<log-line <log-line
:level="level" :level="level"
:filter="filter" :filter="filter"
@@ -74,8 +92,8 @@
:title="$t('metrics')" :title="$t('metrics')"
:execution="execution" :execution="execution"
:key="`metrics-${index}-${taskItem.id}`" :key="`metrics-${index}-${taskItem.id}`"
:data="convertMetric(attempt.metrics)" /> :data="convertMetric(attempt.metrics)"
/>
</template> </template>
<!-- Outputs --> <!-- Outputs -->
<vars <vars
@@ -83,13 +101,10 @@
:title="$t('outputs')" :title="$t('outputs')"
:execution="execution" :execution="execution"
:key="taskItem.id" :key="taskItem.id"
:data="taskItem.outputs" /> :data="taskItem.outputs"
/>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</template> </template>
@@ -104,36 +119,44 @@
import ChartAreaspline from "vue-material-design-icons/ChartAreaspline"; import ChartAreaspline from "vue-material-design-icons/ChartAreaspline";
export default { export default {
components: {LogLine, Restart, Clock, LocationExit, Vars, ChartAreaspline}, components: {
LogLine,
Restart,
Clock,
LocationExit,
Vars,
ChartAreaspline,
},
props: { props: {
level: { level: {
type: String, type: String,
default: "INFO" default: "INFO",
}, },
filter: { filter: {
type: String, type: String,
default: "" default: "",
}, },
taskRunId: { taskRunId: {
type: String, type: String,
default: undefined,
}, },
}, },
data() { data() {
return { return {
showOutputs: {}, showOutputs: {},
showMetrics: {} showMetrics: {},
}; };
}, },
watch: { watch: {
level: function () { level: function () {
this.loadLogs() this.loadLogs();
} },
}, },
created() { created() {
this.loadLogs(); this.loadLogs();
}, },
computed: { computed: {
...mapState("execution", ["execution", "task", "logs"]) ...mapState("execution", ["execution", "task", "logs"]),
}, },
methods: { methods: {
toggleShowOutput(task) { toggleShowOutput(task) {
@@ -141,23 +164,25 @@
this.$forceUpdate(); this.$forceUpdate();
}, },
toggleShowMetric(task, index) { toggleShowMetric(task, index) {
this.showMetrics[task.id + "-" + index] = !this.showMetrics[task.id + "-" + index]; this.showMetrics[task.id + "-" + index] = !this.showMetrics[
task.id + "-" + index
];
this.$forceUpdate(); this.$forceUpdate();
}, },
loadLogs() { loadLogs() {
let params = {minLevel: this.level}; let params = {minLevel: this.level};
if (this.taskRunId) { if (this.taskRunId) {
params.taskRunId = this.taskRunId params.taskRunId = this.taskRunId;
} }
if (this.execution && this.execution.state.current === "RUNNING") { if (this.execution && this.execution.state.current === "RUNNING") {
this.$store this.$store
.dispatch("execution/followLogs", { .dispatch("execution/followLogs", {
id: this.$route.params.id, id: this.$route.params.id,
params: params params: params,
}) })
.then(sse => { .then((sse) => {
this.sse = sse; this.sse = sse;
this.$store.commit("execution/setLogs", []); this.$store.commit("execution/setLogs", []);
@@ -168,23 +193,26 @@
} else { } else {
this.$store.dispatch("execution/loadLogs", { this.$store.dispatch("execution/loadLogs", {
executionId: this.$route.params.id, executionId: this.$route.params.id,
params: params params: params,
}); });
} }
}, },
findLogs(taskRunId, attemptNumber) { findLogs(taskRunId, attemptNumber) {
return (this.logs || []) return (this.logs || []).filter((log) => {
.filter(log => { return (
return log.taskRunId === taskRunId && log.attemptNumber === attemptNumber; log.taskRunId === taskRunId &&
}) log.attemptNumber === attemptNumber
);
});
}, },
convertMetric(metrics) { convertMetric(metrics) {
return (metrics || []) return (metrics || []).reduce((accumulator, r) => {
.reduce((accumulator, r) => { accumulator[r.name] =
accumulator[r.name] = r.type === "timer" ? humanizeDuration(parseInt(r.value * 1000)) : r.value r.type === "timer"
return accumulator; ? humanizeDuration(parseInt(r.value * 1000))
}, Object.create(null)); : r.value;
return accumulator;
}, Object.create(null));
}, },
}, },
beforeDestroy() { beforeDestroy() {
@@ -192,7 +220,7 @@
this.sse.close(); this.sse.close();
this.sse = undefined; this.sse = undefined;
} }
} },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,23 +1,23 @@
<template> <template>
<div> <div>
<log-filters/> <log-filters />
<log-list :level="level" :filter="filterTerm" /> <log-list :level="level" :filter="filterTerm" />
</div> </div>
</template> </template>
<script> <script>
import LogList from "./LogList"; import LogList from "./LogList";
import LogFilters from "./LogFilters"; import LogFilters from "./LogFilters";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
components: { LogList, LogFilters }, components: {LogList, LogFilters},
computed: { computed: {
...mapState("execution", ["execution", "task", "logs"]), ...mapState("execution", ["execution", "task", "logs"]),
filterTerm() { filterTerm() {
return (this.$route.query.q || "").toLowerCase(); return (this.$route.query.q || "").toLowerCase();
},
level() {
return this.$route.query.level || "INFO";
},
}, },
level() { };
return this.$route.query.level || "INFO";
},
},
};
</script> </script>

View File

@@ -3,14 +3,16 @@
<div class="log-content"> <div class="log-content">
<main-log-filter @onChange="loadData" /> <main-log-filter @onChange="loadData" />
<div v-if="logs === undefined"> <div v-if="logs === undefined">
<b-alert variant="light" show>{{$t('no result')}}</b-alert> <b-alert variant="light" show>
{{ $t('no result') }}
</b-alert>
</div> </div>
<div class="bg-dark text-white"> <div class="bg-dark text-white">
<template v-for="(log, i) in logs"> <template v-for="(log, i) in logs">
<log-line <log-line
level="TRACE" level="TRACE"
filter="" filter=""
:excludeMetas="isFlowEdit ? ['namespace', 'flowId'] : []" :exclude-metas="isFlowEdit ? ['namespace', 'flowId'] : []"
:log="log" :log="log"
:key="`${log.taskRunId}-${i}`" :key="`${log.taskRunId}-${i}`"
/> />
@@ -22,59 +24,59 @@
</template> </template>
<script> <script>
import LogLine from "../logs/LogLine"; import LogLine from "../logs/LogLine";
import Pagination from "../layout/Pagination"; import Pagination from "../layout/Pagination";
import { mapState } from "vuex"; import {mapState} from "vuex";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import MainLogFilter from "./MainLogFilter"; import MainLogFilter from "./MainLogFilter";
import qb from "../../utils/queryBuilder"; import qb from "../../utils/queryBuilder";
export default { export default {
mixins: [RouteContext], mixins: [RouteContext],
components: { LogLine, Pagination, MainLogFilter }, components: {LogLine, Pagination, MainLogFilter},
data() { data() {
return {
task: undefined,
};
},
created() {
this.loadData();
},
computed: {
...mapState("log", ["logs", "total", "level"]),
routeInfo() {
return { return {
title: this.$t("logs"), task: undefined,
}; };
}, },
isFlowEdit() { created() {
return this.$route.name === 'flowEdit'
}
},
methods: {
onPageChanged(pagination) {
this.$router.push({
query: { ...this.$route.query, ...pagination },
});
this.loadData(); this.loadData();
}, },
loadData() { computed: {
...mapState("log", ["logs", "total", "level"]),
let q = qb.logQueryBuilder(this.$route); routeInfo() {
if (this.isFlowEdit) { return {
q += ` AND namespace:${this.$route.params.namespace}` title: this.$t("logs"),
q += ` AND flowId:${this.$route.params.id}` };
},
isFlowEdit() {
return this.$route.name === "flowEdit"
} }
this.$store.dispatch("log/findLogs", {
q,
page: this.$route.query.page || 1,
size: this.$route.query.size || 25,
minLevel: this.$route.query.level || "INFO"
});
}, },
}, methods: {
}; onPageChanged(pagination) {
this.$router.push({
query: {...this.$route.query, ...pagination},
});
this.loadData();
},
loadData() {
let q = qb.logQueryBuilder(this.$route);
if (this.isFlowEdit) {
q += ` AND namespace:${this.$route.params.namespace}`
q += ` AND flowId:${this.$route.params.id}`
}
this.$store.dispatch("log/findLogs", {
q,
page: this.$route.query.page || 1,
size: this.$route.query.size || 25,
minLevel: this.$route.query.level || "INFO"
});
},
},
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/_variable.scss"; @import "../../styles/_variable.scss";

View File

@@ -1,6 +1,6 @@
<template> <template>
<b-navbar toggleable="lg" type="light" variant="light"> <b-navbar toggleable="lg" type="light" variant="light">
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle> <b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-nav-form> <b-nav-form>
<search-field ref="searchField" @onSearch="onChange" /> <search-field ref="searchField" @onSearch="onChange" />
@@ -12,17 +12,17 @@
</b-navbar> </b-navbar>
</template> </template>
<script> <script>
import NamespaceSelect from "../namespace/NamespaceSelect"; import NamespaceSelect from "../namespace/NamespaceSelect";
import SearchField from "../layout/SearchField"; import SearchField from "../layout/SearchField";
import DateRange from "../layout/DateRange"; import DateRange from "../layout/DateRange";
import LogLevelSelector from "./LogLevelSelector"; import LogLevelSelector from "./LogLevelSelector";
export default { export default {
components: { NamespaceSelect, DateRange, SearchField, LogLevelSelector }, components: {NamespaceSelect, DateRange, SearchField, LogLevelSelector},
methods: { methods: {
onChange() { onChange() {
this.$emit("onChange"); this.$emit("onChange");
},
}, },
}, };
};
</script> </script>

View File

@@ -5,41 +5,41 @@
:placeholder="$t('Select namespace')" :placeholder="$t('Select namespace')"
:options="namespaces" :options="namespaces"
class="ns-selector" class="ns-selector"
></v-select> />
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
props: { props: {
dataType: { dataType: {
type: String, type: String,
required: true required: true
} }
}, },
created() { created() {
this.$store.dispatch("namespace/loadNamespaces", {dataType: this.dataType}); this.$store.dispatch("namespace/loadNamespaces", {dataType: this.dataType});
this.selectedNamespace = this.$route.query.namespace || ""; this.selectedNamespace = this.$route.query.namespace || "";
}, },
computed: { computed: {
...mapState("namespace", ["namespaces"]) ...mapState("namespace", ["namespaces"])
}, },
data() { data() {
return { return {
selectedNamespace: "" selectedNamespace: ""
}; };
}, },
methods: { methods: {
onNamespaceSelect() { onNamespaceSelect() {
const query = { ...this.$route.query }; const query = {...this.$route.query};
query.namespace = this.selectedNamespace; query.namespace = this.selectedNamespace;
if (!this.selectedNamespace) { if (!this.selectedNamespace) {
delete query.namespace; delete query.namespace;
}
this.$router.push({query});
this.$emit("onNamespaceSelect");
} }
this.$router.push({ query });
this.$emit("onNamespaceSelect");
} }
} };
};
</script> </script>

View File

@@ -2,13 +2,15 @@
<div> <div>
<b-row> <b-row>
<b-col md="9" class="markdown"> <b-col md="9" class="markdown">
<markdown v-if="plugin" :source="plugin.markdown" :permalink="true"></markdown> <markdown v-if="plugin" :source="plugin.markdown" :permalink="true" />
<div v-else> <div v-else>
<b-alert variant="info" show>{{ $t('plugins.please') }}</b-alert> <b-alert variant="info" show>
{{ $t('plugins.please') }}
</b-alert>
</div> </div>
</b-col> </b-col>
<b-col md="3"> <b-col md="3">
<Toc @routerChange="routerChange" v-if="plugins" :plugins="plugins"></Toc> <Toc @routerChange="routerChange" v-if="plugins" :plugins="plugins" />
</b-col> </b-col>
</b-row> </b-row>
</div> </div>
@@ -16,9 +18,9 @@
<script> <script>
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import Markdown from '../layout/Markdown.vue' import Markdown from "../layout/Markdown.vue"
import Toc from './Toc.vue' import Toc from "./Toc.vue"
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
mixins: [RouteContext], mixins: [RouteContext],
@@ -65,7 +67,7 @@
routerChange() { routerChange() {
window.scroll({ window.scroll({
top: 0, top: 0,
behavior: 'smooth' behavior: "smooth"
}) })
this.loadPlugin(); this.loadPlugin();

View File

@@ -2,9 +2,11 @@
<div class="plugins-list"> <div class="plugins-list">
<b-card class="accordion" no-body :key="plugin.manifest['X-Kestra-Title']" v-for="(plugin, index) in plugins"> <b-card class="accordion" no-body :key="plugin.manifest['X-Kestra-Title']" v-for="(plugin, index) in plugins">
<b-card-header header-tag="header" class="p-0" role="tab"> <b-card-header header-tag="header" class="p-0" role="tab">
<b-button block v-b-toggle="plugin.manifest['X-Kestra-Title']" variant="light">{{ plugin.manifest['X-Kestra-Title'] }}</b-button> <b-button block v-b-toggle="plugin.manifest['X-Kestra-Title']" variant="light">
{{ plugin.manifest['X-Kestra-Title'] }}
</b-button>
</b-card-header> </b-card-header>
<b-collapse :id="plugin.manifest['X-Kestra-Title']" :visible="index === 0" accordion="my-accordion" role="tabpanel"> <b-collapse :id="plugin.manifest['X-Kestra-Title']" :visible="index === 0" accordion="my-accordion" role="tabpanel">
<b-card-body> <b-card-body>
<ul class="section-nav toc-h3"> <ul class="section-nav toc-h3">
<li v-for="(classes, namespace) in group(plugin.tasks)" :key="namespace"> <li v-for="(classes, namespace) in group(plugin.tasks)" :key="namespace">
@@ -12,14 +14,13 @@
<ul> <ul>
<li v-for="cls in classes" :key="cls"> <li v-for="cls in classes" :key="cls">
<router-link <router-link
v-on:click.native="$emit('routerChange')" @click.native="$emit('routerChange')"
:to="{ name: 'pluginView', params: {cls: namespace + '.' + cls}}" :to="{name: 'pluginView', params: {cls: namespace + '.' + cls}}"
> >
{{ cls }} {{ cls }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
</li> </li>
</ul> </ul>
</b-card-body> </b-card-body>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<b-form-group :label="$t('Language')" label-cols-sm="3"> <b-form-group :label="$t('Language')" label-cols-sm="3">
<b-form-select v-model="lang" :options="langOptions"></b-form-select> <b-form-select v-model="lang" :options="langOptions" />
</b-form-group> </b-form-group>
<b-form-group :label="$t('Fold auto')" label-cols-sm="3"> <b-form-group :label="$t('Fold auto')" label-cols-sm="3">
<b-checkbox v-model="autofoldTextEditor" value="1" unchecked-value="0" /> <b-checkbox v-model="autofoldTextEditor" value="1" unchecked-value="0" />
@@ -10,45 +10,45 @@
</template> </template>
<script> <script>
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
export default { export default {
mixins: [RouteContext], mixins: [RouteContext],
data() { data() {
return {
langOptions: [
{ value: "en", text: "English" },
{ value: "fr", text: "Français" }
]
};
},
computed: {
routeInfo() {
return { return {
title: this.$t("settings") langOptions: [
{value: "en", text: "English"},
{value: "fr", text: "Français"}
]
}; };
}, },
lang: { computed: {
set(lang) { routeInfo() {
localStorage.setItem("lang", lang); return {
this.$moment.locale(lang); title: this.$t("settings")
this.$root.$i18n.locale = lang; };
this.$toast().saved();
}, },
get() { lang: {
return localStorage.getItem("lang") || "en"; set(lang) {
} localStorage.setItem("lang", lang);
}, this.$moment.locale(lang);
autofoldTextEditor: { this.$root.$i18n.locale = lang;
set(value) { this.$toast().saved();
localStorage.setItem("autofoldTextEditor", value); },
get() {
this.$toast().saved(); return localStorage.getItem("lang") || "en";
}
}, },
get() { autofoldTextEditor: {
return localStorage.getItem('autofoldTextEditor') set(value) {
} localStorage.setItem("autofoldTextEditor", value);
},
} this.$toast().saved();
}; },
get() {
return localStorage.getItem("autofoldTextEditor")
}
},
}
};
</script> </script>

View File

@@ -1,19 +1,20 @@
<template> <template>
<div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady"> <div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady">
<current-chart :data="collections" :options="options"></current-chart> <current-chart :data="collections" :options="options" />
<b-tooltip <b-tooltip
custom-class="tooltip-stats" custom-class="tooltip-stats"
no-fade no-fade
:target="uuid" :target="uuid"
:placement="(this.global ? 'bottom' : 'left')" :placement="(this.global ? 'bottom' : 'left')"
triggers="hover"> triggers="hover"
<span v-html="tooltip"></span> >
<span v-html="tooltip" />
</b-tooltip> </b-tooltip>
</div> </div>
</template> </template>
<script> <script>
import {Line} from 'vue-chartjs' import {Line} from "vue-chartjs"
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
import {tooltip, defaultConfig} from "../../utils/charts.js"; import {tooltip, defaultConfig} from "../../utils/charts.js";
import Utils from "../../utils/utils"; import Utils from "../../utils/utils";
@@ -72,7 +73,7 @@
datasets: [{ datasets: [{
label: "Duration", label: "Duration",
backgroundColor: "#c7e7e5", backgroundColor: "#c7e7e5",
fill: 'start', fill: "start",
pointRadius: 1, pointRadius: 1,
borderWidth: 1, borderWidth: 1,
borderColor: "#1dbaaf", borderColor: "#1dbaaf",

View File

@@ -1,19 +1,20 @@
<template> <template>
<div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady"> <div :id="uuid" :class="'executions-charts' + (this.global ? '' : ' mini')" v-if="dataReady">
<current-chart :data="collections" :options="options"></current-chart> <current-chart :data="collections" :options="options" />
<b-tooltip <b-tooltip
custom-class="tooltip-stats" custom-class="tooltip-stats"
no-fade no-fade
:target="uuid" :target="uuid"
:placement="(this.global ? 'bottom' : 'left')" :placement="(this.global ? 'bottom' : 'left')"
triggers="hover"> triggers="hover"
<span v-html="tooltip"></span> >
<span v-html="tooltip" />
</b-tooltip> </b-tooltip>
</div> </div>
</template> </template>
<script> <script>
import {Bar} from 'vue-chartjs' import {Bar} from "vue-chartjs"
import Utils from "../../utils/utils.js"; import Utils from "../../utils/utils.js";
import {tooltip, defaultConfig} from "../../utils/charts.js"; import {tooltip, defaultConfig} from "../../utils/charts.js";
import State from "../..//utils/state"; import State from "../..//utils/state";

View File

@@ -1,6 +1,8 @@
<template> <template>
<div class="state-global-charts"> <div class="state-global-charts">
<div class="title" :title="$t('last 30 days executions')">{{$t('last 30 days executions')}}</div> <div class="title" :title="$t('last 30 days executions')">
{{ $t('last 30 days executions') }}
</div>
<state-chart <state-chart
v-if="ready" v-if="ready"
:data="data" :data="data"

View File

@@ -2,15 +2,15 @@
<div v-if="ready"> <div v-if="ready">
<data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total" :max="maxTaskRunSetting"> <data-table @onPageChanged="onPageChanged" ref="dataTable" :total="total" :max="maxTaskRunSetting">
<template v-slot:navbar> <template v-slot:navbar>
<search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields"/> <search-field ref="searchField" @onSearch="onSearch" :fields="searchableFields" />
<namespace-select <namespace-select
data-type="flow" data-type="flow"
v-if="$route.name !== 'flowEdit'" v-if="$route.name !== 'flowEdit'"
@onNamespaceSelect="onNamespaceSelect" @onNamespaceSelect="onNamespaceSelect"
/> />
<status-filter-buttons @onRefresh="loadData"/> <status-filter-buttons @onRefresh="loadData" />
<date-range @onDate="onSearch"/> <date-range @onDate="onSearch" />
<refresh-button class="float-right" @onRefresh="loadData"/> <refresh-button class="float-right" @onRefresh="loadData" />
</template> </template>
<template v-slot:top> <template v-slot:top>
@@ -38,16 +38,18 @@
</template> </template>
<template v-slot:cell(details)="row"> <template v-slot:cell(details)="row">
<router-link <router-link
:to="{name: 'executionEdit', params: {namespace: row.item.namespace, flowId: row.item.flowId, id: row.item.executionId},query: {tab:'gantt'} }"> :to="{name: 'executionEdit', params: {namespace: row.item.namespace, flowId: row.item.flowId, id: row.item.executionId},query: {tab:'gantt'}}"
<eye id="edit-action"/> >
<eye id="edit-action" />
</router-link> </router-link>
</template> </template>
<template v-slot:cell(state.startDate)="row">{{ row.item.state.startDate | date('LLLL') }} <template v-slot:cell(state.startDate)="row">
{{ row.item.state.startDate | date('LLLL') }}
</template> </template>
<template v-slot:cell(state.endDate)="row"> <template v-slot:cell(state.endDate)="row">
<span <span
v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)" v-if="!['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ row.item.state.endDate | date('LLLL') }}</span> >{{ row.item.state.endDate | date('LLLL') }}</span>
</template> </template>
<template v-slot:cell(state.current)="row"> <template v-slot:cell(state.current)="row">
<status <status
@@ -57,15 +59,16 @@
/> />
</template> </template>
<template v-slot:cell(state.duration)="row"> <template v-slot:cell(state.duration)="row">
<span <span
v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)" v-if="['RUNNING', 'CREATED'].includes(row.item.state.current)"
>{{ durationFrom(row.item) | humanizeDuration }}</span> >{{ durationFrom(row.item) | humanizeDuration }}</span>
<span v-else>{{ row.item.state.duration | humanizeDuration }}</span> <span v-else>{{ row.item.state.duration | humanizeDuration }}</span>
</template> </template>
<template v-slot:cell(flowId)="row"> <template v-slot:cell(flowId)="row">
<router-link <router-link
:to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}" :to="{name: 'flowEdit', params: {namespace: row.item.namespace, id: row.item.flowId}}"
>{{ row.item.flowId }} >
{{ row.item.flowId }}
</router-link> </router-link>
</template> </template>
<template v-slot:cell(id)="row"> <template v-slot:cell(id)="row">
@@ -75,7 +78,7 @@
<code>{{ row.item.executionId | id }}</code> <code>{{ row.item.executionId | id }}</code>
</template> </template>
<template v-slot:cell(taskId)="row"> <template v-slot:cell(taskId)="row">
<code v-b-tooltip.hover :title=row.item.taskId>{{ row.item.taskId | ellipsis(25) }} </code> <code v-b-tooltip.hover :title="row.item.taskId">{{ row.item.taskId | ellipsis(25) }} </code>
</template> </template>
<template v-slot:cell(executionId)="row"> <template v-slot:cell(executionId)="row">
<code>{{ row.item.executionId | id }}</code> <code>{{ row.item.executionId | id }}</code>
@@ -214,15 +217,15 @@
this.$router.push({ this.$router.push({
name: "executionEdit", name: "executionEdit",
params: {namespace: item.namespace, flowId: item.flowId, id: item.executionId}, params: {namespace: item.namespace, flowId: item.flowId, id: item.executionId},
query: {tab: 'gantt'} query: {tab: "gantt"}
}); });
}, },
loadData(callback) { loadData(callback) {
this.$store this.$store
.dispatch("stat/taskRunDaily", { .dispatch("stat/taskRunDaily", {
q: this.executionQuery, q: this.executionQuery,
startDate: this.$moment(this.startDate).format('YYYY-MM-DD'), startDate: this.$moment(this.startDate).format("YYYY-MM-DD"),
endDate: this.$moment(this.endDate).format('YYYY-MM-DD') endDate: this.$moment(this.endDate).format("YYYY-MM-DD")
}) })
.then(() => { .then(() => {
this.dailyReady = true; this.dailyReady = true;

View File

@@ -1,17 +1,17 @@
<template> <template>
<div> <div>
<editor v-model="content" lang="yaml"></editor> <editor v-model="content" lang="yaml" />
<bottom-line v-if="canSave || canDelete"> <bottom-line v-if="canSave || canDelete">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<b-button class="btn-danger" v-if="canDelete" @click="deleteFile"> <b-button class="btn-danger" v-if="canDelete" @click="deleteFile">
<delete /> <delete />
<span>{{$t('delete')}}</span> <span>{{ $t('delete') }}</span>
</b-button> </b-button>
<b-button @click="save" v-if="canSave"> <b-button @click="save" v-if="canSave">
<content-save /> <content-save />
<span>{{$t('save')}}</span> <span>{{ $t('save') }}</span>
</b-button> </b-button>
</li> </li>
</ul> </ul>
@@ -20,39 +20,39 @@
</template> </template>
<script> <script>
import flowTemplateEdit from "../../mixins/flowTemplateEdit"; import flowTemplateEdit from "../../mixins/flowTemplateEdit";
import { mapState } from "vuex"; import {mapState} from "vuex";
export default { export default {
mixins: [flowTemplateEdit], mixins: [flowTemplateEdit],
data() { data() {
return { return {
dataType: "template", dataType: "template",
}; };
}, },
computed: { computed: {
...mapState("template", ["template"]), ...mapState("template", ["template"]),
}, },
watch: { watch: {
'$route.params'() { "$route.params"() {
this.reload()
},
},
created() {
this.reload() this.reload()
}, },
}, destroyed() {
created() { this.$store.commit("template/setTemplate", undefined);
this.reload() },
}, methods: {
destroyed() { reload() {
this.$store.commit("template/setTemplate", undefined); if (this.$route.name === "templateEdit") {
}, this.$store
methods: { .dispatch("template/loadTemplate", this.$route.params)
reload() { .then(this.loadFile);
if (this.$route.name === "templateEdit") { }
this.$store
.dispatch("template/loadTemplate", this.$route.params)
.then(this.loadFile);
} }
} }
}
}; };
</script> </script>

View File

@@ -36,7 +36,9 @@
<template v-slot:cell(id)="row"> <template v-slot:cell(id)="row">
<router-link <router-link
:to="{name: `${dataType}Edit`, params: {namespace: row.item.namespace, id: row.item.id}}" :to="{name: `${dataType}Edit`, params: {namespace: row.item.namespace, id: row.item.id}}"
>{{row.item.id}}</router-link> >
{{ row.item.id }}
</router-link>
</template> </template>
</b-table> </b-table>
</template> </template>
@@ -48,7 +50,7 @@
<router-link :to="{name: 'templateAdd'}"> <router-link :to="{name: 'templateAdd'}">
<b-button variant="primary"> <b-button variant="primary">
<plus /> <plus />
{{$t('create')}} {{ $t('create') }}
</b-button> </b-button>
</router-link> </router-link>
</li> </li>
@@ -58,75 +60,75 @@
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapState} from "vuex";
import permission from "../../models/permission"; import permission from "../../models/permission";
import action from "../../models/action"; import action from "../../models/action";
import NamespaceSelect from "../namespace/NamespaceSelect"; import NamespaceSelect from "../namespace/NamespaceSelect";
import Plus from "vue-material-design-icons/Plus"; import Plus from "vue-material-design-icons/Plus";
import Eye from "vue-material-design-icons/Eye"; import Eye from "vue-material-design-icons/Eye";
import BottomLine from "../layout/BottomLine"; import BottomLine from "../layout/BottomLine";
import RouteContext from "../../mixins/routeContext"; import RouteContext from "../../mixins/routeContext";
import DataTableActions from "../../mixins/dataTableActions"; import DataTableActions from "../../mixins/dataTableActions";
import DataTable from "../layout/DataTable"; import DataTable from "../layout/DataTable";
import SearchField from "../layout/SearchField"; import SearchField from "../layout/SearchField";
export default { export default {
mixins: [RouteContext, DataTableActions], mixins: [RouteContext, DataTableActions],
components: { components: {
BottomLine, BottomLine,
Plus, Plus,
Eye, Eye,
DataTable, DataTable,
SearchField, SearchField,
NamespaceSelect, NamespaceSelect,
}, },
data() { data() {
return { return {
dataType: "template", dataType: "template",
permission: permission, permission: permission,
action: action, action: action,
};
},
computed: {
...mapState("template", ["templates", "total"]),
...mapState("stat", ["dailyGroupByFlow", "daily"]),
...mapState("auth", ["user"]),
fields() {
const title = (title) => {
return this.$t(title);
}; };
return [
{
key: "id",
label: title("template"),
sortable: true,
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "actions",
label: "",
class: "row-action",
},
];
}, },
}, computed: {
methods: { ...mapState("template", ["templates", "total"]),
loadData(callback) { ...mapState("stat", ["dailyGroupByFlow", "daily"]),
this.$store ...mapState("auth", ["user"]),
.dispatch("template/findTemplates", { fields() {
q: this.query, const title = (title) => {
size: parseInt(this.$route.query.size || 25), return this.$t(title);
page: parseInt(this.$route.query.page || 1), };
sort: this.$route.query.sort, return [
}) {
.then(() => { key: "id",
callback(); label: title("template"),
}); sortable: true,
},
{
key: "namespace",
label: title("namespace"),
sortable: true
},
{
key: "actions",
label: "",
class: "row-action",
},
];
},
}, },
}, methods: {
}; loadData(callback) {
this.$store
.dispatch("template/findTemplates", {
q: this.query,
size: parseInt(this.$route.query.size || 25),
page: parseInt(this.$route.query.page || 1),
sort: this.$route.query.sort,
})
.then(() => {
callback();
});
},
},
};
</script> </script>

View File

@@ -1,40 +1,40 @@
import Vue from 'vue' import Vue from "vue"
import humanizeDuration from "humanize-duration"; import humanizeDuration from "humanize-duration";
Vue.filter('id', value => value ? value.toString().substr(0, 8) : ''); Vue.filter("id", value => value ? value.toString().substr(0, 8) : "");
Vue.filter('humanizeDuration', (value, options) => { Vue.filter("humanizeDuration", (value, options) => {
options = options || { maxDecimalPoints: 2 } options = options || {maxDecimalPoints: 2}
options.spacer = '' options.spacer = ""
var language = localStorage.getItem('lang') || 'en' var language = localStorage.getItem("lang") || "en"
options.language = language options.language = language
options.languages = {} options.languages = {}
options.languages[language] = { options.languages[language] = {
y: () => 'y', y: () => "y",
mo: () => 'mo', mo: () => "mo",
w: () => 'w', w: () => "w",
d: () => 'd', d: () => "d",
h: () => 'h', h: () => "h",
m: () => 'm', m: () => "m",
s: () => 's', s: () => "s",
ms: () => 'ms', ms: () => "ms",
} }
return humanizeDuration(value * 1000, options) return humanizeDuration(value * 1000, options)
}); });
Vue.filter('cap', value => value ? value.toString().capitalize() : ''); Vue.filter("cap", value => value ? value.toString().capitalize() : "");
Vue.filter('lower', value => value ? value.toString().toLowerCase() : ''); Vue.filter("lower", value => value ? value.toString().toLowerCase() : "");
Vue.filter('date', (dateString, format) => { Vue.filter("date", (dateString, format) => {
let f; let f;
if (format === 'full') { if (format === "full") {
f = 'MMMM Do YYYY, h: mm: ss' f = "MMMM Do YYYY, h: mm: ss"
} else if (format === 'human') { } else if (format === "human") {
f = 'LLLL' f = "LLLL"
} else { } else {
f = format f = format
} }
return Vue.moment(dateString).format(f) return Vue.moment(dateString).format(f)
}) })
Vue.filter('ellipsis', (text, len) => text.length > len ? text.substr(0, len) + '...' : text.substr(0, len)) Vue.filter("ellipsis", (text, len) => text.length > len ? text.substr(0, len) + "..." : text.substr(0, len))

View File

@@ -1,6 +1,6 @@
import Vue from 'vue'; import Vue from "vue";
import VueAxios from 'vue-axios'; import VueAxios from "vue-axios";
import axios from 'axios'; import axios from "axios";
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
let root = (process.env.VUE_APP_API_URL || "") + KESTRA_BASE_PATH; let root = (process.env.VUE_APP_API_URL || "") + KESTRA_BASE_PATH;
@@ -12,7 +12,7 @@ export default (callback, store, nprogress) => {
const instance = axios.create({ const instance = axios.create({
timeout: 15000, timeout: 15000,
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json"
}, },
onUploadProgress: function (progressEvent) { onUploadProgress: function (progressEvent) {
if (progressEvent && progressEvent.loaded && progressEvent.total) { if (progressEvent && progressEvent.loaded && progressEvent.total) {
@@ -27,7 +27,7 @@ export default (callback, store, nprogress) => {
return response return response
}, errorResponse => { }, errorResponse => {
if (errorResponse.response && errorResponse.response.data) { if (errorResponse.response && errorResponse.response.data) {
store.dispatch('core/showErrorMessage', errorResponse.response.data) store.dispatch("core/showErrorMessage", errorResponse.response.data)
} }
return Promise.reject(errorResponse); return Promise.reject(errorResponse);

View File

@@ -1,34 +1,34 @@
import './filters' import "./filters"
import 'moment/locale/fr' import "moment/locale/fr"
import './utils/global' import "./utils/global"
import './custom.scss' import "./custom.scss"
// @TODO: move to scss // @TODO: move to scss
import 'vue-material-design-icons/styles.css'; import "vue-material-design-icons/styles.css";
import App from './App.vue' import App from "./App.vue"
import BootstrapVue from 'bootstrap-vue' import BootstrapVue from "bootstrap-vue"
import Vue from 'vue' import Vue from "vue"
import VueI18n from 'vue-i18n' import VueI18n from "vue-i18n"
import VueMoment from 'vue-moment' import VueMoment from "vue-moment"
import NProgress from 'vue-nprogress' import NProgress from "vue-nprogress"
import VueRouter from 'vue-router' import VueRouter from "vue-router"
import VueSSE from 'vue-sse'; import VueSSE from "vue-sse";
import VueSidebarMenu from 'vue-sidebar-menu' import VueSidebarMenu from "vue-sidebar-menu"
import Vuex from "vuex"; import Vuex from "vuex";
import VueAnalytics from 'vue-analytics'; import VueAnalytics from "vue-analytics";
import configureHttp from './http' import configureHttp from "./http"
import Toast from "./utils/toast"; import Toast from "./utils/toast";
import { extendMoment } from 'moment-range'; import {extendMoment} from "moment-range";
import Translations from './translations.json' import Translations from "./translations.json"
import moment from 'moment' import moment from "moment"
import routes from './routes/routes' import routes from "./routes/routes"
import stores from './stores/store' import stores from "./stores/store"
import vSelect from 'vue-select' import vSelect from "vue-select"
import VueHotkey from 'v-hotkey' import VueHotkey from "v-hotkey"
let app = document.querySelector('#app'); let app = document.querySelector("#app");
if (app) { if (app) {
Vue.use(Vuex) Vue.use(Vuex)
@@ -49,7 +49,7 @@ if (app) {
Vue.use(VueI18n); Vue.use(VueI18n);
let i18n = new VueI18n({ let i18n = new VueI18n({
locale: localStorage.getItem('lang') || 'en', locale: localStorage.getItem("lang") || "en",
messages: Translations messages: Translations
}); });
@@ -60,13 +60,13 @@ if (app) {
Vue.use(VueHotkey) Vue.use(VueHotkey)
Vue.use(VueSSE); Vue.use(VueSSE);
Vue.use(VueMoment, { moment: extendMoment(moment) }); Vue.use(VueMoment, {moment: extendMoment(moment)});
Vue.use(VueSidebarMenu); Vue.use(VueSidebarMenu);
Vue.use(BootstrapVue); Vue.use(BootstrapVue);
Vue.use(Toast) Vue.use(Toast)
Vue.component('v-select', vSelect); Vue.component("v-select", vSelect);
Vue.config.productionTip = false; Vue.config.productionTip = false;

View File

@@ -15,7 +15,7 @@ export default {
computed: { computed: {
routeInfo() { routeInfo() {
return { return {
title: this.$t(this.dataType + 's') title: this.$t(this.dataType + "s")
}; };
}, },
storageName() { storageName() {
@@ -25,7 +25,7 @@ export default {
return this.fields.filter(f => f.sortable); return this.fields.filter(f => f.sortable);
}, },
isBasePage() { isBasePage() {
return ['executionsList', 'flowsList'].includes(this.$route.name) return ["executionsList", "flowsList"].includes(this.$route.name)
} }
}, },
methods: { methods: {
@@ -39,13 +39,13 @@ export default {
`${sortItem.sortBy}:${sortItem.sortDesc ? "desc" : "asc"}` `${sortItem.sortBy}:${sortItem.sortDesc ? "desc" : "asc"}`
]; ];
this.$router.push({ this.$router.push({
query: { ...this.$route.query, sort } query: {...this.$route.query, sort}
}); });
this.loadData(this.onDataLoaded); this.loadData(this.onDataLoaded);
this.saveFilters() this.saveFilters()
}, },
onRowDoubleClick(item) { onRowDoubleClick(item) {
this.$router.push({ name: this.dataType + "Edit", params: item }); this.$router.push({name: this.dataType + "Edit", params: item});
}, },
onPageChanged(item) { onPageChanged(item) {
this.$router.push({ this.$router.push({
@@ -76,10 +76,10 @@ export default {
} }
}, },
loadFilters () { loadFilters () {
const query = { ...this.$route.query} const query = {...this.$route.query}
let change = false let change = false
if (this.isBasePage) { if (this.isBasePage) {
const userPreferences = JSON.parse(localStorage.getItem(this.storageName) || '{}') const userPreferences = JSON.parse(localStorage.getItem(this.storageName) || "{}")
for (const key in userPreferences) { for (const key in userPreferences) {
if (!query[key] && userPreferences[key]) { if (!query[key] && userPreferences[key]) {
query[key] = userPreferences[key] query[key] = userPreferences[key]
@@ -88,7 +88,7 @@ export default {
} }
} }
if (change) { if (change) {
this.$router.push({ query: query }); this.$router.push({query: query});
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { canSaveFlowTemplate, saveFlowTemplate } from "../utils/flowTemplate"; import {canSaveFlowTemplate, saveFlowTemplate} from "../utils/flowTemplate";
import { mapGetters, mapState } from "vuex"; import {mapGetters, mapState} from "vuex";
import BottomLine from "../components/layout/BottomLine"; import BottomLine from "../components/layout/BottomLine";
import ContentSave from "vue-material-design-icons/ContentSave"; import ContentSave from "vue-material-design-icons/ContentSave";
@@ -124,12 +124,12 @@ export default {
} else { } else {
const item = YamlUtils.parse(this.content); const item = YamlUtils.parse(this.content);
this.$store this.$store
.dispatch(`${this.dataType}/create${this.dataType.capitalize()}`, { [this.dataType]: item}) .dispatch(`${this.dataType}/create${this.dataType.capitalize()}`, {[this.dataType]: item})
.then(() => { .then(() => {
this.$router.push({ this.$router.push({
name: `${this.dataType}Edit`, name: `${this.dataType}Edit`,
params: item, params: item,
query: { tab: "data-source" } query: {tab: "data-source"}
}); });
}) })
.then(() => { .then(() => {

View File

@@ -16,7 +16,7 @@ export default {
methods: { methods: {
onLoad() { onLoad() {
if (!this.preventRouteInfo) { if (!this.preventRouteInfo) {
this.$store.commit('layout/setTopNavbar', this.routeInfo) this.$store.commit("layout/setTopNavbar", this.routeInfo)
let baseTitle; let baseTitle;

View File

@@ -1,145 +1,145 @@
<template> <template>
<sidebar-menu <sidebar-menu
:menu="menu" :menu="menu"
@toggle-collapse="onToggleCollapse" @toggle-collapse="onToggleCollapse"
width="200px" width="200px"
:collapsed="collapsed" :collapsed="collapsed"
> >
<div class="logo" slot="header"> <div class="logo" slot="header">
<router-link :to="{name: 'home'}"> <router-link :to="{name: 'home'}">
<span> <span>
<span class="img" /> <span class="img" />
</span>
</router-link>
</div>
<span slot="toggle-icon">
<chevron-right v-if="collapsed" />
<chevron-left v-else />
</span> </span>
</router-link> </sidebar-menu>
</div>
<span slot="toggle-icon">
<chevron-right v-if="collapsed" />
<chevron-left v-else />
</span>
</sidebar-menu>
</template> </template>
<script> <script>
import Vue from "vue"; import Vue from "vue";
import { SidebarMenu } from "vue-sidebar-menu"; import {SidebarMenu} from "vue-sidebar-menu";
import ChevronLeft from "vue-material-design-icons/ChevronLeft"; import ChevronLeft from "vue-material-design-icons/ChevronLeft";
import ChevronRight from "vue-material-design-icons/ChevronRight"; import ChevronRight from "vue-material-design-icons/ChevronRight";
import Graph from "vue-material-design-icons/Graph"; import Graph from "vue-material-design-icons/Graph";
import Cog from "vue-material-design-icons/Cog"; import Cog from "vue-material-design-icons/Cog";
import TimelineClock from "vue-material-design-icons/TimelineClock"; import TimelineClock from "vue-material-design-icons/TimelineClock";
import BookOpen from "vue-material-design-icons/BookOpen"; import BookOpen from "vue-material-design-icons/BookOpen";
import CardText from "vue-material-design-icons/CardText"; import CardText from "vue-material-design-icons/CardText";
import HexagonMultiple from "vue-material-design-icons/HexagonMultiple"; import HexagonMultiple from "vue-material-design-icons/HexagonMultiple";
import ChartTimeline from "vue-material-design-icons/ChartTimeline"; import ChartTimeline from "vue-material-design-icons/ChartTimeline";
Vue.component("graph", Graph); Vue.component("graph", Graph);
Vue.component("settings", Cog); Vue.component("settings", Cog);
Vue.component("timelineclock", TimelineClock); Vue.component("timelineclock", TimelineClock);
Vue.component("bookopen", BookOpen); Vue.component("bookopen", BookOpen);
Vue.component("cardtext", CardText); Vue.component("cardtext", CardText);
Vue.component("hexagon-multiple", HexagonMultiple); Vue.component("hexagon-multiple", HexagonMultiple);
Vue.component("charttimeline", ChartTimeline); Vue.component("charttimeline", ChartTimeline);
export default { export default {
components: { components: {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
SidebarMenu, SidebarMenu,
},
methods: {
onToggleCollapse(folded) {
this.collapsed = folded;
localStorage.setItem("menuCollapsed", folded ? "true" : "false");
this.$emit("onMenuCollapse", folded);
}
},
data() {
return {
collapsed: localStorage.getItem("menuCollapsed") === "true"
};
},
computed: {
menu() {
return [
{
href: "/flows",
alias: [
"/flows*"
],
title: this.$t("flows"),
icon: {
element: "graph",
class: "menu-icon"
}
}, },
{ methods: {
href: "/templates", onToggleCollapse(folded) {
alias: [ this.collapsed = folded;
"/templates*" localStorage.setItem("menuCollapsed", folded ? "true" : "false");
], this.$emit("onMenuCollapse", folded);
title: this.$t("templates"),
icon: {
element: "cardtext",
class: "menu-icon"
} }
}, },
{ data() {
href: "/executions", return {
alias: [ collapsed: localStorage.getItem("menuCollapsed") === "true"
"/executions*" };
],
title: this.$t("executions"),
icon: {
element: "timelineclock",
class: "menu-icon"
}
}, },
{ computed: {
href: "/taskruns", menu() {
alias: ["/taskruns*"], return [
title: this.$t("taskruns"), {
icon: { href: "/flows",
element: "charttimeline", alias: [
class: "menu-icon" "/flows*"
} ],
}, title: this.$t("flows"),
{ icon: {
href: "/logs", element: "graph",
alias: [ class: "menu-icon"
"/logs*" }
], },
title: this.$t("logs"), {
icon: { href: "/templates",
element: "hexagon-multiple", alias: [
class: "menu-icon" "/templates*"
} ],
}, title: this.$t("templates"),
{ icon: {
href: "/plugins", element: "cardtext",
alias: [ class: "menu-icon"
"/plugins*" }
], },
title: this.$t("plugins.documentation"), {
icon: { href: "/executions",
element: "bookopen", alias: [
class: "menu-icon" "/executions*"
} ],
}, title: this.$t("executions"),
{ icon: {
href: "/settings", element: "timelineclock",
alias: [ class: "menu-icon"
"/settings*" }
], },
title: this.$t("settings"), {
icon: { href: "/taskruns",
element: "settings", alias: ["/taskruns*"],
class: "menu-icon" title: this.$t("taskruns"),
} icon: {
element: "charttimeline",
class: "menu-icon"
}
},
{
href: "/logs",
alias: [
"/logs*"
],
title: this.$t("logs"),
icon: {
element: "hexagon-multiple",
class: "menu-icon"
}
},
{
href: "/plugins",
alias: [
"/plugins*"
],
title: this.$t("plugins.documentation"),
icon: {
element: "bookopen",
class: "menu-icon"
}
},
{
href: "/settings",
alias: [
"/settings*"
],
title: this.$t("settings"),
icon: {
element: "settings",
class: "menu-icon"
}
}
];
}
} }
]; };
}
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div/> <div />
</template> </template>
<script> <script>
export default { export default {

View File

@@ -1,46 +1,46 @@
import ExecutionRoot from '../components/executions/ExecutionRoot.vue' import ExecutionRoot from "../components/executions/ExecutionRoot.vue"
import Executions from '../components/executions/Executions.vue' import Executions from "../components/executions/Executions.vue"
import TaskRuns from '../components/taskruns/TaskRuns.vue' import TaskRuns from "../components/taskruns/TaskRuns.vue"
import FlowEdit from '../components/flows/FlowEdit.vue' import FlowEdit from "../components/flows/FlowEdit.vue"
import FlowRoot from '../components/flows/FlowRoot.vue' import FlowRoot from "../components/flows/FlowRoot.vue"
import Flows from '../components/flows/Flows.vue' import Flows from "../components/flows/Flows.vue"
import LogsWrapper from '../components/logs/LogsWrapper.vue' import LogsWrapper from "../components/logs/LogsWrapper.vue"
import Plugin from '../components/plugins/Plugin.vue' import Plugin from "../components/plugins/Plugin.vue"
import Settings from '../components/settings/Settings.vue' import Settings from "../components/settings/Settings.vue"
import TemplateEdit from '../components/templates/TemplateEdit.vue' import TemplateEdit from "../components/templates/TemplateEdit.vue"
import Templates from '../components/templates/Templates.vue' import Templates from "../components/templates/Templates.vue"
export default { export default {
mode: 'history', mode: "history",
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
base: KESTRA_UI_PATH, base: KESTRA_UI_PATH,
routes: [ routes: [
//Flows //Flows
{ name: 'home', path: '/', component: Flows }, {name: "home", path: "/", component: Flows},
{ name: 'flowsList', path: '/flows', component: Flows }, {name: "flowsList", path: "/flows", component: Flows},
{ name: 'flowsAdd', path: '/flows/new', component: FlowEdit }, {name: "flowsAdd", path: "/flows/new", component: FlowEdit},
{ name: 'flowEdit', path: '/flows/edit/:namespace/:id', component: FlowRoot }, {name: "flowEdit", path: "/flows/edit/:namespace/:id", component: FlowRoot},
//Executions //Executions
{ name: 'executionsList', path: '/executions', component: Executions }, {name: "executionsList", path: "/executions", component: Executions},
{ name: 'executionEdit', path: '/executions/:namespace/:flowId/:id', component: ExecutionRoot }, {name: "executionEdit", path: "/executions/:namespace/:flowId/:id", component: ExecutionRoot},
//TaskRuns //TaskRuns
{ name: 'taskRunList', path: '/taskruns', component: TaskRuns }, {name: "taskRunList", path: "/taskruns", component: TaskRuns},
//Documentation //Documentation
{ name: 'plugin', path: '/plugins', component: Plugin }, {name: "plugin", path: "/plugins", component: Plugin},
{ name: 'pluginView', path: '/plugins/:cls', component: Plugin }, {name: "pluginView", path: "/plugins/:cls", component: Plugin},
//Templates //Templates
{ name: 'templateList', path: '/templates', component: Templates }, {name: "templateList", path: "/templates", component: Templates},
{ name: 'templateAdd', path: '/templates/new', component: TemplateEdit }, {name: "templateAdd", path: "/templates/new", component: TemplateEdit},
{ name: 'templateEdit', path: '/templates/edit/:namespace/:id', component: TemplateEdit }, {name: "templateEdit", path: "/templates/edit/:namespace/:id", component: TemplateEdit},
//Settings //Settings
{ name: 'logs', path: '/logs', component: LogsWrapper }, {name: "logs", path: "/logs", component: LogsWrapper},
//Settings //Settings
{ name: 'settings', path: '/settings', component: Settings }, {name: "settings", path: "/settings", component: Settings},
] ]
} }

View File

@@ -5,7 +5,7 @@ export default {
}, },
actions: { actions: {
showErrorMessage({commit}, message) { showErrorMessage({commit}, message) {
commit('setErrorMessage', message) commit("setErrorMessage", message)
} }
}, },
mutations: { mutations: {

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -10,68 +10,68 @@ export default {
logs: [] logs: []
}, },
actions: { actions: {
loadExecutions({ commit }, options) { loadExecutions({commit}, options) {
return Vue.axios.get(`/api/v1/executions`, { params: options }).then(response => { return Vue.axios.get("/api/v1/executions", {params: options}).then(response => {
commit('setExecutions', response.data.results) commit("setExecutions", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
}) })
}, },
restartExecution(_, options) { restartExecution(_, options) {
return Vue.axios.post(`/api/v1/executions/${options.id}/restart?taskId=${options.taskId}`, { params: options }, { return Vue.axios.post(`/api/v1/executions/${options.id}/restart?taskId=${options.taskId}`, {params: options}, {
headers: { headers: {
'content-type': 'multipart/form-data' "content-type": "multipart/form-data"
} }
}) })
}, },
kill(_, options) { kill(_, options) {
return Vue.axios.delete(`/api/v1/executions/${options.id}/kill`); return Vue.axios.delete(`/api/v1/executions/${options.id}/kill`);
}, },
loadExecution({ commit }, options) { loadExecution({commit}, options) {
return Vue.axios.get(`/api/v1/executions/${options.id}`).then(response => { return Vue.axios.get(`/api/v1/executions/${options.id}`).then(response => {
commit('setExecution', response.data) commit("setExecution", response.data)
}) })
}, },
findExecutions({ commit }, options) { findExecutions({commit}, options) {
const sort = options.sort const sort = options.sort
delete options.sort delete options.sort
let sortQueryString = '' let sortQueryString = ""
if (sort) { if (sort) {
sortQueryString = `?sort=${sort}` sortQueryString = `?sort=${sort}`
} }
return Vue.axios.get(`/api/v1/executions/search${sortQueryString}`, { params: options }).then(response => { return Vue.axios.get(`/api/v1/executions/search${sortQueryString}`, {params: options}).then(response => {
commit('setExecutions', response.data.results) commit("setExecutions", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
}) })
}, },
triggerExecution(_, options) { triggerExecution(_, options) {
return Vue.axios.post(`/api/v1/executions/trigger/${options.namespace}/${options.id}`, options.formData, { return Vue.axios.post(`/api/v1/executions/trigger/${options.namespace}/${options.id}`, options.formData, {
timeout: 60 * 60 * 1000, timeout: 60 * 60 * 1000,
headers: { headers: {
'content-type': 'multipart/form-data' "content-type": "multipart/form-data"
} }
}) })
}, },
createFlow({ commit }, options) { createFlow({commit}, options) {
return Vue.axios.post('/api/v1/executions', options.execution).then(response => { return Vue.axios.post("/api/v1/executions", options.execution).then(response => {
commit('setFlow', response.data.flow) commit("setFlow", response.data.flow)
}) })
}, },
followExecution(_, options) { followExecution(_, options) {
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/executions/${options.id}/follow`, { format: 'json' }) return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/executions/${options.id}/follow`, {format: "json"})
}, },
followLogs(_, options) { followLogs(_, options) {
return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/logs/${options.id}/follow`, { format: 'json', params: options.params }) return Vue.SSE(`${Vue.axios.defaults.baseURL}api/v1/logs/${options.id}/follow`, {format: "json", params: options.params})
}, },
loadTree({ commit }, execution) { loadTree({commit}, execution) {
return Vue.axios.get(`/api/v1/executions/${execution.id}/tree`).then(response => { return Vue.axios.get(`/api/v1/executions/${execution.id}/tree`).then(response => {
commit('setDataTree', response.data.tasks) commit("setDataTree", response.data.tasks)
}) })
}, },
loadLogs({ commit }, options) { loadLogs({commit}, options) {
return Vue.axios.get(`/api/v1/logs/${options.executionId}`, { return Vue.axios.get(`/api/v1/logs/${options.executionId}`, {
params: options.params params: options.params
}).then(response => { }).then(response => {
commit('setLogs', response.data) commit("setLogs", response.data)
}) })
} }
}, },

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -10,58 +10,58 @@ export default {
}, },
actions: { actions: {
findFlows({ commit }, options) { findFlows({commit}, options) {
const sortString = options.sort ? `?sort=${options.sort}` : '' const sortString = options.sort ? `?sort=${options.sort}` : ""
delete options.sort delete options.sort
return Vue.axios.get(`/api/v1/flows/search${sortString}`, { return Vue.axios.get(`/api/v1/flows/search${sortString}`, {
params: options params: options
}).then(response => { }).then(response => {
commit('setFlows', response.data.results) commit("setFlows", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
return response.data; return response.data;
}) })
}, },
loadFlow({ commit }, options) { loadFlow({commit}, options) {
return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}`).then(response => { return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}`).then(response => {
commit('setFlow', response.data) commit("setFlow", response.data)
return response.data; return response.data;
}) })
}, },
saveFlow({ commit }, options) { saveFlow({commit}, options) {
return Vue.axios.put(`/api/v1/flows/${options.flow.namespace}/${options.flow.id}`, options.flow).then(response => { return Vue.axios.put(`/api/v1/flows/${options.flow.namespace}/${options.flow.id}`, options.flow).then(response => {
if (response.status >= 300) { if (response.status >= 300) {
return Promise.reject(new Error("Server error on flow save")) return Promise.reject(new Error("Server error on flow save"))
} else { } else {
commit('setFlow', response.data) commit("setFlow", response.data)
return response.data; return response.data;
} }
}) })
}, },
createFlow({ commit }, options) { createFlow({commit}, options) {
return Vue.axios.post('/api/v1/flows', options.flow).then(response => { return Vue.axios.post("/api/v1/flows", options.flow).then(response => {
commit('setFlow', response.data) commit("setFlow", response.data)
return response.data; return response.data;
}) })
}, },
deleteFlow({ commit }, flow) { deleteFlow({commit}, flow) {
return Vue.axios.delete(`/api/v1/flows/${flow.namespace}/${flow.id}`).then(() => { return Vue.axios.delete(`/api/v1/flows/${flow.namespace}/${flow.id}`).then(() => {
commit('setFlow', null) commit("setFlow", null)
}) })
}, },
loadTree({ commit }, flow) { loadTree({commit}, flow) {
return Vue.axios.get(`/api/v1/flows/${flow.namespace}/${flow.id}/tree`).then(response => { return Vue.axios.get(`/api/v1/flows/${flow.namespace}/${flow.id}/tree`).then(response => {
commit('setDataTree', response.data.tasks) commit("setDataTree", response.data.tasks)
return response.data.tasks; return response.data.tasks;
}) })
}, },
loadRevisions({ commit }, options) { loadRevisions({commit}, options) {
return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}/revisions`).then(response => { return Vue.axios.get(`/api/v1/flows/${options.namespace}/${options.id}/revisions`).then(response => {
commit('setRevisions', response.data) commit("setRevisions", response.data)
return response.data; return response.data;
}) })

View File

@@ -5,17 +5,17 @@ export default {
configurationPanelPosition: undefined, configurationPanelPosition: undefined,
}, },
actions: { actions: {
updateConfigurationPanelPosition({ commit, state }) { updateConfigurationPanelPosition({commit, state}) {
if (state.node) { if (state.node) {
const position = { const position = {
right: state.node.getCTM().e + 95, right: state.node.getCTM().e + 95,
top: state.node.getCTM().f + 10 top: state.node.getCTM().f + 10
} }
commit('setConfigurationPanelPosition', position) commit("setConfigurationPanelPosition", position)
} }
}, },
setNode({commit}, node) { setNode({commit}, node) {
commit('setNode', node) commit("setNode", node)
// dispatch('updateConfigurationPanelPosition') // dispatch('updateConfigurationPanelPosition')
}, },
}, },

View File

@@ -1,16 +1,16 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
logs: undefined, logs: undefined,
total: 0, total: 0,
level: 'INFO' level: "INFO"
}, },
actions: { actions: {
findLogs({ commit }, options) { findLogs({commit}, options) {
return Vue.axios.get(`/api/v1/logs/search`, { params: options }).then(response => { return Vue.axios.get("/api/v1/logs/search", {params: options}).then(response => {
commit('setLogs', response.data.results) commit("setLogs", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
}) })
}, },
}, },

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -6,9 +6,9 @@ export default {
}, },
actions: { actions: {
loadNamespaces({ commit }, options) { loadNamespaces({commit}, options) {
return Vue.axios.get(`/api/v1/${options.dataType}s/distinct-namespaces`).then(response => { return Vue.axios.get(`/api/v1/${options.dataType}s/distinct-namespaces`).then(response => {
commit('setNamespaces', response.data) commit("setNamespaces", response.data)
}) })
}, },
}, },

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
@@ -7,14 +7,14 @@ export default {
plugins: undefined, plugins: undefined,
}, },
actions: { actions: {
list({ commit }) { list({commit}) {
return Vue.axios.get(`/api/v1/plugins`).then(response => { return Vue.axios.get("/api/v1/plugins").then(response => {
commit('setPlugins', response.data) commit("setPlugins", response.data)
}) })
}, },
load({ commit }, options) { load({commit}, options) {
return Vue.axios.get(`/api/v1/plugins/${options.cls}`).then(response => { return Vue.axios.get(`/api/v1/plugins/${options.cls}`).then(response => {
commit('setPlugin', response.data) commit("setPlugin", response.data)
}) })
}, },

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -7,23 +7,23 @@ export default {
taskRunDaily: undefined taskRunDaily: undefined
}, },
actions: { actions: {
dailyGroupByFlow({ commit }, payload) { dailyGroupByFlow({commit}, payload) {
return Vue.axios.post(`/api/v1/stats/executions/daily/group-by-flow`, payload).then(response => { return Vue.axios.post("/api/v1/stats/executions/daily/group-by-flow", payload).then(response => {
commit('setDailyGroupByFlow', response.data) commit("setDailyGroupByFlow", response.data)
return response.data; return response.data;
}) })
}, },
daily({ commit }, payload) { daily({commit}, payload) {
return Vue.axios.post(`/api/v1/stats/executions/daily`, payload).then(response => { return Vue.axios.post("/api/v1/stats/executions/daily", payload).then(response => {
commit('setDaily', response.data) commit("setDaily", response.data)
return response.data; return response.data;
}) })
}, },
taskRunDaily({ commit }, payload) { taskRunDaily({commit}, payload) {
return Vue.axios.post(`/api/v1/stats/taskruns/daily`, payload).then(response => { return Vue.axios.post("/api/v1/stats/taskruns/daily", payload).then(response => {
commit('setTaskRunDaily', response.data) commit("setTaskRunDaily", response.data)
return response.data; return response.data;
}) })

View File

@@ -1,16 +1,16 @@
import auth from './auth' import auth from "./auth"
import core from './core' import core from "./core"
import execution from './executions' import execution from "./executions"
import flow from './flow' import flow from "./flow"
import graph from './graph' import graph from "./graph"
import layout from './layout' import layout from "./layout"
import log from './logs' import log from "./logs"
import namespace from './namespaces' import namespace from "./namespaces"
import plugin from './plugins' import plugin from "./plugins"
import settings from './settings' import settings from "./settings"
import stat from './stat' import stat from "./stat"
import template from './template' import template from "./template"
import taskrun from './taskruns' import taskrun from "./taskruns"
export default { export default {
modules: { modules: {

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -7,21 +7,21 @@ export default {
maxTaskRunSetting: 100 maxTaskRunSetting: 100
}, },
actions: { actions: {
findTaskRuns({ commit }, options) { findTaskRuns({commit}, options) {
const sort = options.sort const sort = options.sort
delete options.sort delete options.sort
let sortQueryString = '' let sortQueryString = ""
if (sort) { if (sort) {
sortQueryString = `?sort=${sort}` sortQueryString = `?sort=${sort}`
} }
return Vue.axios.get(`/api/v1/taskruns/search${sortQueryString}`, { params: options }).then(response => { return Vue.axios.get(`/api/v1/taskruns/search${sortQueryString}`, {params: options}).then(response => {
commit('setTaskruns', response.data.results) commit("setTaskruns", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
}) })
}, },
maxTaskRunSetting({ commit }) { maxTaskRunSetting({commit}) {
return Vue.axios.get(`/api/v1/taskruns/maxTaskRunSetting`).then(response => { return Vue.axios.get("/api/v1/taskruns/maxTaskRunSetting").then(response => {
commit('setMaxTaskRunSetting', response.data) commit("setMaxTaskRunSetting", response.data)
}) })
} }
}, },

View File

@@ -1,4 +1,4 @@
import Vue from 'vue' import Vue from "vue"
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@@ -8,46 +8,46 @@ export default {
}, },
actions: { actions: {
findTemplates({ commit }, options) { findTemplates({commit}, options) {
const sortString = options.sort ? `?sort=${options.sort}` : '' const sortString = options.sort ? `?sort=${options.sort}` : ""
delete options.sort delete options.sort
return Vue.axios.get(`/api/v1/templates/search${sortString}`, { return Vue.axios.get(`/api/v1/templates/search${sortString}`, {
params: options params: options
}).then(response => { }).then(response => {
commit('setTemplates', response.data.results) commit("setTemplates", response.data.results)
commit('setTotal', response.data.total) commit("setTotal", response.data.total)
return response.data; return response.data;
}) })
}, },
loadTemplate({ commit }, options) { loadTemplate({commit}, options) {
return Vue.axios.get(`/api/v1/templates/${options.namespace}/${options.id}`).then(response => { return Vue.axios.get(`/api/v1/templates/${options.namespace}/${options.id}`).then(response => {
commit('setTemplate', response.data) commit("setTemplate", response.data)
return response.data; return response.data;
}) })
}, },
saveTemplate({ commit }, options) { saveTemplate({commit}, options) {
return Vue.axios.put(`/api/v1/templates/${options.template.namespace}/${options.template.id}`, options.template).then(response => { return Vue.axios.put(`/api/v1/templates/${options.template.namespace}/${options.template.id}`, options.template).then(response => {
if (response.status >= 300) { if (response.status >= 300) {
return Promise.reject(new Error("Server error on template save")) return Promise.reject(new Error("Server error on template save"))
} else { } else {
commit('setTemplate', response.data) commit("setTemplate", response.data)
return response.data; return response.data;
} }
}) })
}, },
createTemplate({ commit }, options) { createTemplate({commit}, options) {
return Vue.axios.post('/api/v1/templates', options.template).then(response => { return Vue.axios.post("/api/v1/templates", options.template).then(response => {
commit('setTemplate', response.data) commit("setTemplate", response.data)
return response.data; return response.data;
}) })
}, },
deleteTemplate({ commit }, template) { deleteTemplate({commit}, template) {
return Vue.axios.delete(`/api/v1/templates/${template.namespace}/${template.id}`).then(() => { return Vue.axios.delete(`/api/v1/templates/${template.namespace}/${template.id}`).then(() => {
commit('setTemplate', null) commit("setTemplate", null)
}) })
}, },
}, },

View File

@@ -5,18 +5,18 @@ export function tooltip(tooltipModel) {
const bodyLines = (tooltipModel.body || []).map(r => r.lines); const bodyLines = (tooltipModel.body || []).map(r => r.lines);
if (tooltipModel.body) { if (tooltipModel.body) {
let innerHtml = ''; let innerHtml = "";
titleLines.forEach(function (title) { titleLines.forEach(function (title) {
innerHtml += '<h6>' + title + '</h6>'; innerHtml += "<h6>" + title + "</h6>";
}); });
bodyLines.forEach(function (body, i) { bodyLines.forEach(function (body, i) {
let colors = tooltipModel.labelColors[i]; let colors = tooltipModel.labelColors[i];
let style = 'background:' + colors.backgroundColor; let style = "background:" + colors.backgroundColor;
style += '; border-color:' + colors.borderColor; style += "; border-color:" + colors.borderColor;
let span = '<span class="square" style="' + style + '"></span>'; let span = "<span class=\"square\" style=\"" + style + "\"></span>";
innerHtml += span + body + '<br />'; innerHtml += span + body + "<br />";
}); });
return innerHtml; return innerHtml;
@@ -36,7 +36,7 @@ export function defaultConfig(overide) {
} }
}, },
tooltips: { tooltips: {
mode: 'index', mode: "index",
intersect: false, intersect: false,
enabled: false, enabled: false,
}, },

View File

@@ -19,7 +19,7 @@ export function canSaveFlowTemplate(isEdit, user, item, dataType) {
export function saveFlowTemplate(self, file, dataType) { export function saveFlowTemplate(self, file, dataType) {
return self.$store return self.$store
.dispatch(`${dataType}/save${dataType.capitalize()}`, { [dataType]: file }) .dispatch(`${dataType}/save${dataType.capitalize()}`, {[dataType]: file})
.then((response) => { .then((response) => {
self.$toast().saved(response.id); self.$toast().saved(response.id);
}) })

View File

@@ -12,5 +12,5 @@ String.prototype.hashCode = function () {
hash = ((hash << 5) - hash) + char; hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer hash = hash & hash; // Convert to 32bit integer
} }
return hash + ''; return hash + "";
} }

View File

@@ -22,11 +22,11 @@ export default class Markdown {
breaks: true, breaks: true,
linkify: true, linkify: true,
typographer: true, typographer: true,
langPrefix: 'language-', langPrefix: "language-",
quotes: '“”‘’', quotes: "“”‘’",
}) })
md.renderer.rules.table_open = () => `<table class="table table-bordered">\n` md.renderer.rules.table_open = () => "<table class=\"table table-bordered\">\n"
return md.render( return md.render(
markdown markdown

View File

@@ -34,13 +34,13 @@ export default class QueryBuilder {
query.push(QueryBuilder.toLucene(q.q)); query.push(QueryBuilder.toLucene(q.q));
} }
return query.join(" AND ") || '*' return query.join(" AND ") || "*"
} }
static logQueryBuilder(route) { static logQueryBuilder(route) {
const q = route.query const q = route.query
const start = q.start ? iso(q.start) : '*' const start = q.start ? iso(q.start) : "*"
const end = q.end ? iso(q.end) : '*' const end = q.end ? iso(q.end) : "*"
return [ return [
`${q.q ? QueryBuilder.toLucene(q.q) : "*"}`, `${q.q ? QueryBuilder.toLucene(q.q) : "*"}`,
`timestamp:[${start} TO ${end}]`, `timestamp:[${start} TO ${end}]`,

View File

@@ -58,11 +58,11 @@ export default class State {
static icon() { static icon() {
return { return {
[State.CREATED]: "pause-circle-outline", [State.CREATED]: "pause-circle-outline",
[State.SUCCESS]: 'check-circle-outline', [State.SUCCESS]: "check-circle-outline",
[State.RUNNING]: 'play-circle-outline', [State.RUNNING]: "play-circle-outline",
[State.FAILED]: 'close-circle-outline', [State.FAILED]: "close-circle-outline",
[State.KILLING]: 'close-circle-outline', [State.KILLING]: "close-circle-outline",
[State.KILLED]: 'stop-circle-outline' [State.KILLED]: "stop-circle-outline"
}; };
} }
} }

View File

@@ -5,7 +5,7 @@ export default {
return { return {
_wrap: function(message) { _wrap: function(message) {
return [self.$createElement('span', {domProps: {innerHTML: message}})]; return [self.$createElement("span", {domProps: {innerHTML: message}})];
}, },
confirm: function(message, callback) { confirm: function(message, callback) {
return self.$bvModal return self.$bvModal

View File

@@ -12,9 +12,9 @@ export default class Utils {
return [] return []
.concat(...Object .concat(...Object
.keys(child) .keys(child)
.map(key => typeof child[key] === 'object' ? .map(key => typeof child[key] === "object" ?
_flatten(child[key], path.concat([key])) : _flatten(child[key], path.concat([key])) :
({ [path.concat([key]).join(".")] : child[key] }) ({[path.concat([key]).join(".")] : child[key]})
) )
); );
}(object)); }(object));
@@ -31,12 +31,12 @@ export default class Utils {
if (typeof(flat[key]) === "string") { if (typeof(flat[key]) === "string") {
let date = moment(flat[key], moment.ISO_8601); let date = moment(flat[key], moment.ISO_8601);
if (date.isValid()) { if (date.isValid()) {
return {key, value: date.format('LLLL')}; return {key, value: date.format("LLLL")};
} }
} }
if (typeof(flat[key]) === "number") { if (typeof(flat[key]) === "number") {
return {key, value: flat[key].toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ')}; return {key, value: flat[key].toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1 ")};
} }
return {key, value: flat[key]}; return {key, value: flat[key]};

View File

@@ -1,4 +1,4 @@
import JsYaml from 'js-yaml'; import JsYaml from "js-yaml";
import _cloneDeep from "lodash/cloneDeep"; import _cloneDeep from "lodash/cloneDeep";
export default class YamlUtils { export default class YamlUtils {
@@ -26,7 +26,7 @@ export default class YamlUtils {
} else if (typeof(value) === "string" || value instanceof String) { } else if (typeof(value) === "string" || value instanceof String) {
value = value value = value
.replaceAll("\t", " ") .replaceAll("\t", " ")
.replaceAll(/\u00A0/g, ' '); .replaceAll(/\u00A0/g, " ");
if (value.indexOf("\\n") >= 0) { if (value.indexOf("\\n") >= 0) {
return value.replaceAll("\\n", "\n") + "\n"; return value.replaceAll("\\n", "\n") + "\n";

View File

@@ -1,4 +1,4 @@
const path = require('path'); const path = require("path");
module.exports = { module.exports = {
publicPath: "/ui/", publicPath: "/ui/",
@@ -6,18 +6,14 @@ module.exports = {
configureWebpack: { configureWebpack: {
resolve: { resolve: {
alias: { alias: {
Override: path.resolve(__dirname, 'src/override/') Override: path.resolve(__dirname, "src/override/")
} }
}, },
module: { module: {
rules: [ rules: [
{ {
test: /\.sass$/, test: /\.sass$/,
use: [ use: ["vue-style-loader", "css-loader", "sass-loader"]
'vue-style-loader',
'css-loader',
'sass-loader'
]
} }
] ]
}, },