Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): refreshing the main dashboard #4959

Merged
merged 31 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
98e4f96
feat(ui): initial work on refreshing the dashboard
MilosPaunovic Sep 17, 2024
f2cb78a
feat(ui): provide a real duration input that validate it and generate…
Skraye Sep 17, 2024
d8c6254
chore(ui): improve labeling of namespaces in listing (#4957)
MilosPaunovic Sep 17, 2024
fa141a1
Merge branch 'develop' into dashboard
MilosPaunovic Sep 17, 2024
48ce461
feat(ui): more work on dashboard reworking
MilosPaunovic Sep 18, 2024
26c5c76
chore(ui): add duration toggle to executions graph
MilosPaunovic Sep 18, 2024
7d467d0
feat: implement refresh token (#4833)
Skraye Sep 17, 2024
225aa02
chore(deps-dev): bump vite from 5.4.5 to 5.4.6 in /ui (#4963)
dependabot[bot] Sep 17, 2024
c3f6817
chore(ui): improve the main layout of dashboard
MilosPaunovic Sep 18, 2024
17e0230
chore(ui): completed the executions main chart
MilosPaunovic Sep 18, 2024
00d0e7a
chore(ui): initial work on doughnut chart
MilosPaunovic Sep 18, 2024
6744318
chore(ui): add doughnut chart
MilosPaunovic Sep 18, 2024
48b3f0b
chore(ui): add center plugin for doughnut chart
MilosPaunovic Sep 18, 2024
6166b47
feat(*): schedule an execution on a fixed date
loicmathieu Sep 16, 2024
b41e616
fix(webserver): missing params in file paths (#4966)
Skraye Sep 18, 2024
4c0e718
fix(core): make flow plugin defaults override global ones
yuri1969 Sep 11, 2024
7590132
feat(ui): warn usage of trigger variable in flow when executing (#4969)
Skraye Sep 18, 2024
3e26fd3
fix(core): always add the secret consumer
loicmathieu Sep 18, 2024
4ee1168
fix(jdbc): retry flaky tests forEachItem
loicmathieu Sep 18, 2024
aeef841
Auto-generate translations from en.json (#4970)
github-actions[bot] Sep 18, 2024
bb3f52f
feat(core); log the stacktrace in case of plugin scanning error
loicmathieu Sep 18, 2024
388a1bc
fix(ui): missing translation (#4968)
github-actions[bot] Sep 18, 2024
57fb1d2
fix(core): SchedulerScheduleOnDatesTest.recoverNoneMissing flakiness
loicmathieu Sep 19, 2024
8207e0b
feat(ui): initial work on refreshing the dashboard
MilosPaunovic Sep 17, 2024
ed32bb9
feat(ui): more work on dashboard reworking
MilosPaunovic Sep 18, 2024
fb3bb91
feat(ui): work on main dashboard
MilosPaunovic Sep 19, 2024
1cec4ad
chore(ui): work on the main dashboard
MilosPaunovic Sep 20, 2024
d210be5
chore(ui): remove obsolete file
MilosPaunovic Sep 20, 2024
a053177
Merge branch 'develop' into dashboard
MilosPaunovic Sep 20, 2024
787193c
chore(ui): added filters
MilosPaunovic Sep 20, 2024
e0bdc48
chore(ui): finisihing otuches for new dashboard
MilosPaunovic Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ void recoverLASTMissing() throws Exception {
// needed for RetryingTest to work since there is no context cleaning between method => we have to clear assertion receiver manually
receive.blockLast();

assertThat(queueCount.getCount(), is(0L));
assertThat(queueCount.getCount(), is(0L));
Trigger newTrigger = this.triggerState.findLast(lastTrigger).orElseThrow();
// depending on the exact timing of events, the trigger date can be before or after
assertThat(newTrigger.getDate().toLocalDateTime(), oneOf(before.toLocalDateTime(), after.toLocalDateTime()));
Expand Down
390 changes: 390 additions & 0 deletions ui/src/components/dashboard/Dashboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
<template>
<Header v-if="!embed" />

<div class="filters">
<el-row :gutter="10" class="mx-0">
<el-col :xs="24" :lg="4">
<namespace-select
v-model="filters.namespace"
:data-type="'flow'"
:disabled="!!props.flow || !!props.namespace"
@update:model-value="updateParams()"
/>
</el-col>
<el-col :xs="24" :lg="4">
<el-select
v-model="filters.state"
clearable
filterable
collapse-tags
multiple
:placeholder="$t('state')"
@update:model-value="updateParams()"
>
<el-option
v-for="item in State.allStates()"
:key="item.key"
:label="item.key"
:value="item.key"
/>
</el-select>
</el-col>
<el-col :xs="24" :lg="8">
<date-filter
@update:is-relative="toggleAutoRefresh"
@update:filter-value="(dates) => updateParams(dates)"
absolute
class="d-flex flex-row"
/>
</el-col>
<el-col :xs="24" :sm="16" :lg="4">
<scope-filter-buttons
v-model="filters.scope"
:label="$t('data')"
@update:model-value="updateParams()"
/>
</el-col>
<el-col :xs="24" :sm="8" :lg="4">
<refresh-button
class="float-right"
@refresh="fetchAll()"
:can-auto-refresh="canAutoRefresh"
/>
</el-col>
</el-row>
</div>

<div class="dashboard">
<el-row :gutter="20" class="mx-0">
<el-col :xs="24" :sm="12" :lg="6">
<Card
:icon="CheckBold"
:label="t('dashboard.success_ratio')"
:value="stats.success"
:redirect="{
name: 'executions/list',
query: {state: State.SUCCESS, scope: 'USER'},
}"
/>
</el-col>
<el-col :xs="24" :sm="12" :lg="6">
<Card
:icon="Alert"
:label="t('dashboard.failure_ratio')"
:value="stats.failed"
:redirect="{
name: 'executions/list',
query: {state: State.FAILED, scope: 'USER'},
}"
/>
</el-col>
<el-col :xs="24" :sm="12" :lg="6">
<Card
:icon="FileTree"
:label="t('flows')"
:value="numbers.flows"
:redirect="{
name: 'flows/list',
query: {scope: 'USER'},
}"
/>
</el-col>
<el-col :xs="24" :sm="12" :lg="6">
<Card
:icon="LightningBolt"
:label="t('triggers')"
:value="numbers.triggers"
:redirect="{
name: 'admin/triggers',
}"
/>
</el-col>
</el-row>

<el-row :gutter="20" class="mx-0">
<el-col :xs="24" :lg="16">
<ExecutionsBar :data="graphData" :total="stats.total" />
</el-col>
<el-col :xs="24" :lg="8">
<ExecutionsDoughnut :data="graphData" :total="stats.total" />
</el-col>
</el-row>

<el-row :gutter="20" class="mx-0">
<el-col :xs="24" :lg="12">
<ExecutionsInProgress
:flow="props.flow"
:namespace="props.namespace"
/>
</el-col>
<el-col :xs="24" :lg="12">
<ExecutionsNextScheduled
:flow="props.flow"
:namespace="props.namespace"
/>
</el-col>
</el-row>

<el-row :gutter="20" class="mx-0">
<el-col :xs="24">
<ExecutionsNamespace
:data="namespaceExecutions"
:total="stats.total"
/>
</el-col>
</el-row>

<el-row :gutter="20" class="mx-0">
<el-col :xs="24">
<Logs :data="logs" />
</el-col>
</el-row>
</div>
</template>

<script setup>
import {onBeforeMount, ref, computed} from "vue";
import {useRouter} from "vue-router";
import {useStore} from "vuex";
import {useI18n} from "vue-i18n";

import moment from "moment";
import _cloneDeep from "lodash/cloneDeep";

import {apiUrl} from "override/utils/route";
import State from "../../utils/state";

import Header from "./components/Header.vue";
import Card from "./components/Card.vue";

import NamespaceSelect from "../namespace/NamespaceSelect.vue";
import DateFilter from "../executions/date-select/DateFilter.vue";
import ScopeFilterButtons from "../layout/ScopeFilterButtons.vue";
import RefreshButton from "../layout/RefreshButton.vue";

import ExecutionsBar from "./components/charts/executions/Bar.vue";
import ExecutionsDoughnut from "./components/charts/executions/Doughnut.vue";
import ExecutionsNamespace from "./components/charts/executions/Namespace.vue";
import Logs from "./components/charts/logs/Bar.vue";

import ExecutionsInProgress from "./components/tables/executions/InProgress.vue";
import ExecutionsNextScheduled from "./components/tables/executions/NextScheduled.vue";

import CheckBold from "vue-material-design-icons/CheckBold.vue";
import Alert from "vue-material-design-icons/Alert.vue";
import LightningBolt from "vue-material-design-icons/LightningBolt.vue";
import FileTree from "vue-material-design-icons/FileTree.vue";

const router = useRouter();
const store = useStore();
const {t} = useI18n({useScope: "global"});

const props = defineProps({
embed: {
type: Boolean,
default: false,
},
flow: {
type: String,
required: false,
default: null,
},
namespace: {
type: String,
required: false,
default: null,
},
});

const filters = ref({
namespace: null,
state: [],
startDate: null,
endDate: null,
timeRange: "PT720H",
scope: ["USER"],
});

const canAutoRefresh = ref(false);
const toggleAutoRefresh = (event) => {
canAutoRefresh.value = event;
};

const numbers = ref({flows: 0, triggers: 0});
const fetchNumbers = () => {
store.$http
.post(`${apiUrl(store)}/stats/summary`, filters.value)
.then((response) => {
if (!response.data) return;
numbers.value = response.data;
});
};

const executions = ref({raw: {}, all: {}, yesterday: {}, today: {}});
const stats = computed(() => {
const counts = executions?.value?.all?.executionCounts || {};
const total = Object.values(counts).reduce((sum, count) => sum + count, 0);

function percentage(count, total) {
return total ? Math.round((count / total) * 100) : 0;
}

return {
total,
success: `${percentage(counts[State.SUCCESS] || 0, total)}%`,
failed: `${percentage(counts[State.FAILED] || 0, total)}%`,
};
});
const transformer = (data) => {
return data.reduce((accumulator, value) => {
if (!accumulator) accumulator = _cloneDeep(value);
else {
for (const key in value.executionCounts) {
accumulator.executionCounts[key] += value.executionCounts[key];
}

for (const key in value.duration) {
accumulator.duration[key] += value.duration[key];
}
}

return accumulator;
}, null);
};
const fetchExecutions = () => {
store.dispatch("stat/daily", filters.value).then((response) => {
const sorted = response.sort(
(a, b) => new Date(b.date) - new Date(a.date),
);

executions.value = {
raw: sorted,
all: transformer(sorted),
yesterday: sorted.at(-2),
today: sorted.at(-1),
};
});
};

const graphData = computed(() => store.state.stat.daily || []);

const namespaceExecutions = ref({});
const fetchNamespaceExecutions = () => {
store
.dispatch("stat/dailyGroupByFlow", {
...filters.value,
namespaceOnly: true,
})
.then((response) => {
namespaceExecutions.value = response;
});
};

const logs = ref([]);
const fetchLogs = () => {
store.dispatch("stat/logDaily", filters.value).then((response) => {
logs.value = response;
});
};

const handleDatesUpdate = (dates) => {
const {startDate, endDate, timeRange} = dates;

if (startDate && endDate) {
filters.value = {...filters.value, startDate, endDate, timeRange};
} else if (timeRange) {
filters.value = {
...filters.value,
startDate: moment()
.subtract(moment.duration(timeRange).as("milliseconds"))
.toISOString(true),
endDate: moment().toISOString(true),
timeRange,
};
}

return Promise.resolve(filters.value);
};

const updateParams = async (params) => {
const completeParams = await handleDatesUpdate({
...filters.value,
...params,
});

filters.value = {
namespace: props.namespace ?? completeParams.namespace,
flowId: props.flow ?? null,
state: completeParams.state?.filter(Boolean).length
? [].concat(completeParams.state)
: undefined,
startDate: completeParams.startDate,
endDate: completeParams.endDate,
scope: completeParams.scope?.filter(Boolean).length
? [].concat(completeParams.scope)
: undefined,
};

completeParams.flowId = props.flow ?? null;

delete completeParams.timeRange;
for (const key in completeParams) {
if (completeParams[key] == null) {
delete completeParams[key];
}
}

router.push({query: completeParams}).then(fetchAll());
};

const fetchAll = async () => {
try {
await Promise.any([
fetchNumbers(),
fetchExecutions(),
fetchNamespaceExecutions(),
fetchLogs(),
]);
} catch (error) {
console.error("All promises failed:", error);
}
};

onBeforeMount(() => updateParams());
</script>

<style lang="scss" scoped>
@import "@kestra-io/ui-libs/src/scss/variables";

$spacing: 20px;

.filters,
.dashboard {
padding: $spacing;

& .el-row {
width: 100%;

& .el-col {
padding-bottom: $spacing;

& div {
border-radius: $border-radius;
background: var(--card-bg);
}
}
}
}

.filters {
padding-bottom: 0;

& .el-row {
padding: 0 5px;
}

& .el-col {
padding-bottom: 0 !important;
}
}
</style>
Loading
Loading