mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-26 05:00:31 -05:00
Compare commits
1 Commits
dependabot
...
chore/reve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc8a9fe6cf |
@@ -373,7 +373,6 @@
|
||||
import SelectTable from "../layout/SelectTable.vue";
|
||||
import TriggerAvatar from "../flows/TriggerAvatar.vue";
|
||||
import KSFilter from "../filter/components/KSFilter.vue";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import MarkdownTooltip from "../layout/MarkdownTooltip.vue";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
|
||||
@@ -474,8 +473,6 @@
|
||||
.filter(Boolean) as ColumnConfig[]
|
||||
);
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl();
|
||||
|
||||
const loadData = (callback?: () => void) => {
|
||||
const query = loadQuery({
|
||||
size: parseInt(String(route.query?.size ?? "25")),
|
||||
@@ -501,8 +498,7 @@
|
||||
|
||||
const {ready, onSort, onPageChanged, queryWithFilter, load} = useDataTableActions({
|
||||
dataTableRef: dataTable,
|
||||
loadData,
|
||||
saveRestoreUrl
|
||||
loadData
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections ref="dashboardComponent" :dashboard="{id: 'default', charts: []}" :charts showDefault />
|
||||
<Sections ref="dashboardComponent" :dashboard="{id: 'default', charts: []}" :charts showDefault class="mb-4" />
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
@@ -384,7 +384,7 @@
|
||||
import _merge from "lodash/merge";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {ref, computed, onMounted, watch, h, useTemplateRef} from "vue";
|
||||
import {ref, computed, watch, h, useTemplateRef} from "vue";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import {ElMessageBox, ElSwitch, ElFormItem, ElAlert, ElCheckbox} from "element-plus";
|
||||
|
||||
@@ -423,18 +423,17 @@
|
||||
import {filterValidLabels} from "./utils";
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
import {humanizeDuration, invisibleSpace} from "../../utils/filters";
|
||||
import Utils from "../../utils/utils";
|
||||
|
||||
import action from "../../models/action";
|
||||
import permission from "../../models/permission";
|
||||
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
import {useTableColumns} from "../../composables/useTableColumns";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
import {useApplyDefaultFilter} from "../filter/composables/useDefaultFilter";
|
||||
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
@@ -495,7 +494,6 @@
|
||||
const selectedStatus = ref(undefined);
|
||||
const lastRefreshDate = ref(new Date());
|
||||
const unqueueDialogVisible = ref(false);
|
||||
const isDefaultNamespaceAllow = ref(true);
|
||||
const changeStatusDialogVisible = ref(false);
|
||||
const actionOptions = ref<Record<string, any>>({});
|
||||
const dblClickRouteName = ref("executions/update");
|
||||
@@ -613,11 +611,6 @@
|
||||
const routeInfo = computed(() => ({title: t("executions")}));
|
||||
useRouteContext(routeInfo, props.embed);
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl({
|
||||
restoreUrl: true,
|
||||
isDefaultNamespaceAllow: isDefaultNamespaceAllow.value
|
||||
});
|
||||
|
||||
const dataTableRef = ref(null);
|
||||
const selectTableRef = useTemplateRef<typeof SelectTable>("selectTable");
|
||||
|
||||
@@ -633,8 +626,7 @@
|
||||
dblClickRouteName: dblClickRouteName.value,
|
||||
embed: props.embed,
|
||||
dataTableRef,
|
||||
loadData: loadData,
|
||||
saveRestoreUrl
|
||||
loadData: loadData
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -1042,29 +1034,10 @@
|
||||
emit("state-count", {runningCount, totalCount});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const query = {...route.query};
|
||||
let queryHasChanged = false;
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
||||
query["filters[scope][EQUALS]"] = "USER";
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (queryHasChanged) {
|
||||
router.replace({query});
|
||||
}
|
||||
|
||||
if (route.name === "flows/update") {
|
||||
optionalColumns.value = optionalColumns.value.
|
||||
filter(col => col.prop !== "namespace" && col.prop !== "flowId");
|
||||
}
|
||||
useApplyDefaultFilter({
|
||||
namespace: props.namespace,
|
||||
includeTimeRange: true,
|
||||
includeScope: true
|
||||
});
|
||||
|
||||
watch(isOpenLabelsModal, (opening) => {
|
||||
|
||||
70
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
70
ui/src/components/filter/composables/useDefaultFilter.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {onMounted} from "vue";
|
||||
import {LocationQuery, useRoute, useRouter} from "vue-router";
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
import {defaultNamespace} from "../../../composables/useNamespaces";
|
||||
|
||||
interface DefaultFilterOptions {
|
||||
namespace?: string;
|
||||
includeTimeRange?: boolean;
|
||||
includeScope?: boolean;
|
||||
legacyQuery?: boolean;
|
||||
}
|
||||
|
||||
const NAMESPACE_FILTER_PREFIX = "filters[namespace]";
|
||||
const SCOPE_FILTER_PREFIX = "filters[scope]";
|
||||
const TIME_RANGE_FILTER_PREFIX = "filters[timeRange]";
|
||||
|
||||
const hasFilterKey = (query: LocationQuery, prefix: string): boolean =>
|
||||
Object.keys(query).some(key => key.startsWith(prefix));
|
||||
|
||||
export function applyDefaultFilters(
|
||||
currentQuery: LocationQuery,
|
||||
options: DefaultFilterOptions & {
|
||||
configuration?: any;
|
||||
route?: any
|
||||
} = {}): { query: LocationQuery; hasChanges: boolean } {
|
||||
|
||||
const {configuration, route, namespace, includeTimeRange, includeScope, legacyQuery = false} = options;
|
||||
|
||||
const hasTimeRange = configuration && route
|
||||
? configuration.keys?.some((k: any) => k.key === "timeRange") ?? false
|
||||
: includeTimeRange ?? false;
|
||||
const hasScope = configuration && route
|
||||
? route?.name !== "logs/list" && (configuration.keys?.some((k: any) => k.key === "scope") ?? false)
|
||||
: includeScope ?? false;
|
||||
|
||||
const query = {...currentQuery};
|
||||
let hasChanges = false;
|
||||
|
||||
if (namespace === undefined && defaultNamespace() && !hasFilterKey(query, NAMESPACE_FILTER_PREFIX)) {
|
||||
query[legacyQuery ? "namespace" : `${NAMESPACE_FILTER_PREFIX}[PREFIX]`] = defaultNamespace();
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasScope && !hasFilterKey(query, SCOPE_FILTER_PREFIX)) {
|
||||
query[legacyQuery ? "scope" : `${SCOPE_FILTER_PREFIX}[EQUALS]`] = "USER";
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
const TIME_FILTER_KEYS = /startDate|endDate|timeRange/;
|
||||
|
||||
if (hasTimeRange && !Object.keys(query).some(key => TIME_FILTER_KEYS.test(key))) {
|
||||
const defaultDuration = useMiscStore().configs?.chartDefaultDuration ?? "P30D";
|
||||
query[legacyQuery ? "timeRange" : `${TIME_RANGE_FILTER_PREFIX}[EQUALS]`] = defaultDuration;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return {query, hasChanges};
|
||||
}
|
||||
|
||||
export function useApplyDefaultFilter(options?: DefaultFilterOptions) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
const {query, hasChanges} = applyDefaultFilters(route.query, options);
|
||||
if (hasChanges) {
|
||||
router.replace({query});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
KV_COMPARATORS
|
||||
} from "../utils/filterTypes";
|
||||
import {usePreAppliedFilters} from "./usePreAppliedFilters";
|
||||
import {applyDefaultFilters} from "./useDefaultFilter";
|
||||
|
||||
export function useFilters(configuration: FilterConfiguration, showSearchInput = true, legacyQuery = false) {
|
||||
const router = useRouter();
|
||||
@@ -28,8 +29,7 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
const {
|
||||
markAsPreApplied,
|
||||
hasPreApplied,
|
||||
getPreApplied,
|
||||
getAllPreApplied
|
||||
getPreApplied
|
||||
} = usePreAppliedFilters();
|
||||
|
||||
const appendQueryParam = (query: Record<string, any>, key: string, value: string) => {
|
||||
@@ -367,13 +367,10 @@ export function useFilters(configuration: FilterConfiguration, showSearchInput =
|
||||
updateRoute();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets all filters to their pre-applied state and clears the search query
|
||||
*/
|
||||
const resetToPreApplied = () => {
|
||||
appliedFilters.value = getAllPreApplied();
|
||||
const defaultQuery = applyDefaultFilters({}, {configuration, route, legacyQuery}).query;
|
||||
searchQuery.value = "";
|
||||
updateRoute();
|
||||
router.push({query: defaultQuery});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const useNamespacesFilter = (): ComputedRef<FilterConfiguration> => compu
|
||||
const {t} = useI18n();
|
||||
|
||||
return {
|
||||
title: t("filter.titles.namespaces_filters"),
|
||||
title: t("filter.titles.namespace_filters"),
|
||||
searchPlaceholder: t("filter.search_placeholders.search_namespaces"),
|
||||
keys: [],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
:namespace="flowStore.flow?.namespace"
|
||||
:flowId="flowStore.flow?.id"
|
||||
:topbar="false"
|
||||
:restoreUrl="false"
|
||||
filter
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -249,8 +249,8 @@
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, useTemplateRef} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {ref, computed, useTemplateRef} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import _merge from "lodash/merge";
|
||||
import * as FILTERS from "../../utils/filters";
|
||||
@@ -284,7 +284,6 @@
|
||||
import permission from "../../models/permission";
|
||||
|
||||
import {useToast} from "../../utils/toast";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
|
||||
import {useFlowStore} from "../../stores/flow";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
@@ -294,7 +293,7 @@
|
||||
import {useTableColumns} from "../../composables/useTableColumns";
|
||||
import {DataTableRef, useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import {useSelectTableActions} from "../../composables/useSelectTableActions";
|
||||
|
||||
import {useApplyDefaultFilter} from "../filter/composables/useDefaultFilter";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
topbar?: boolean;
|
||||
@@ -312,7 +311,6 @@
|
||||
const miscStore = useMiscStore();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const {t} = useI18n();
|
||||
const toast = useToast()
|
||||
@@ -497,6 +495,11 @@
|
||||
updateVisibleColumns(newColumns);
|
||||
}
|
||||
|
||||
useApplyDefaultFilter({
|
||||
namespace: props.namespace,
|
||||
includeScope: true
|
||||
});
|
||||
|
||||
function exportFlows() {
|
||||
toast.confirm(
|
||||
t("flow export", {flowCount: queryBulkAction.value ? flowStore.total : selection.value.length}),
|
||||
@@ -633,25 +636,6 @@
|
||||
operation: "EQUALS"
|
||||
}];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const query = {...route.query};
|
||||
const queryKeys = Object.keys(query);
|
||||
let queryHasChanged = false;
|
||||
|
||||
if (props.namespace === undefined && defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (!queryKeys.some(key => key.startsWith("filters[scope]"))) {
|
||||
query["filters[scope][EQUALS]"] = "USER";
|
||||
queryHasChanged = true;
|
||||
}
|
||||
|
||||
if (queryHasChanged) router.replace({query});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
import DataTable from "../layout/DataTable.vue";
|
||||
import SearchField from "../layout/SearchField.vue";
|
||||
import NamespaceSelect from "../namespaces/components/NamespaceSelect.vue";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
|
||||
@@ -77,11 +76,9 @@
|
||||
}));
|
||||
|
||||
useRouteContext(routeInfo);
|
||||
const {saveRestoreUrl} = useRestoreUrl({restoreUrl: true, isDefaultNamespaceAllow: true});
|
||||
|
||||
const {onPageChanged, onDataTableValue, queryWithFilter, ready} = useDataTableActions({
|
||||
loadData,
|
||||
saveRestoreUrl
|
||||
loadData
|
||||
});
|
||||
|
||||
const namespace = computed({
|
||||
|
||||
@@ -272,7 +272,6 @@
|
||||
import DataTable from "../layout/DataTable.vue";
|
||||
import _merge from "lodash/merge";
|
||||
import {type DataTableRef, useDataTableActions} from "../../composables/useDataTableActions.ts";
|
||||
|
||||
const dataTable = useTemplateRef<DataTableRef>("dataTable");
|
||||
|
||||
const loadData = async (callback?: () => void) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TopNavBar v-if="!embed" :title="routeInfo.title" />
|
||||
<section v-bind="$attrs" :class="{'container': !embed}" class="log-panel">
|
||||
<div class="log-content">
|
||||
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="pageSize" :page="pageNumber" :embed="embed">
|
||||
<DataTable @page-changed="onPageChanged" ref="dataTable" :total="logsStore.total" :size="internalPageSize" :page="internalPageNumber" :embed="embed">
|
||||
<template #navbar v-if="!embed || showFilters">
|
||||
<KSFilter
|
||||
:configuration="logFilter"
|
||||
@@ -15,12 +15,12 @@
|
||||
</template>
|
||||
|
||||
<template v-if="showStatChart()" #top>
|
||||
<Sections ref="dashboard" :charts :dashboard="{id: 'default', charts: []}" showDefault />
|
||||
<Sections ref="dashboardRef" :charts :dashboard="{id: 'default', charts: []}" showDefault class="mb-4" />
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<div v-loading="isLoading">
|
||||
<div v-if="logsStore.logs !== undefined && logsStore.logs.length > 0" class="logs-wrapper">
|
||||
<div v-if="logsStore.logs !== undefined && logsStore.logs?.length > 0" class="logs-wrapper">
|
||||
<LogLine
|
||||
v-for="(log, i) in logsStore.logs"
|
||||
:key="`${log.taskRunId}-${i}`"
|
||||
@@ -42,6 +42,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, watch} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import _merge from "lodash/merge";
|
||||
import moment from "moment";
|
||||
import {useLogFilter} from "../filter/configurations";
|
||||
import KSFilter from "../filter/components/KSFilter.vue";
|
||||
import Sections from "../dashboard/sections/Sections.vue";
|
||||
@@ -49,193 +54,151 @@
|
||||
import TopNavBar from "../../components/layout/TopNavBar.vue";
|
||||
import LogLine from "../logs/LogLine.vue";
|
||||
import NoData from "../layout/NoData.vue";
|
||||
|
||||
const logFilter = useLogFilter();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {mapStores} from "pinia";
|
||||
import RouteContext from "../../mixins/routeContext";
|
||||
import RestoreUrl from "../../mixins/restoreUrl";
|
||||
import DataTableActions from "../../mixins/dataTableActions";
|
||||
import _merge from "lodash/merge";
|
||||
import {storageKeys} from "../../utils/constants";
|
||||
import {decodeSearchParams} from "../filter/utils/helpers";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import YAML_CHART from "../dashboard/assets/logs_timeseries_chart.yaml?raw";
|
||||
import {useLogsStore} from "../../stores/logs";
|
||||
import {defaultNamespace} from "../../composables/useNamespaces";
|
||||
import {defineComponent} from "vue";
|
||||
import {useDataTableActions} from "../../composables/useDataTableActions";
|
||||
import useRouteContext from "../../composables/useRouteContext";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [RouteContext, RestoreUrl, DataTableActions],
|
||||
props: {
|
||||
logLevel: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
embed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showFilters: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
reloadLogs: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDefaultNamespaceAllow: true,
|
||||
task: undefined,
|
||||
isLoading: false,
|
||||
lastRefreshDate: new Date(),
|
||||
canAutoRefresh: false,
|
||||
showChart: localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
storageKeys() {
|
||||
return storageKeys
|
||||
},
|
||||
...mapStores(useLogsStore),
|
||||
routeInfo() {
|
||||
return {
|
||||
title: this.$t("logs"),
|
||||
};
|
||||
},
|
||||
isFlowEdit() {
|
||||
return this.$route.name === "flows/update"
|
||||
},
|
||||
isNamespaceEdit() {
|
||||
return this.$route.name === "namespaces/update"
|
||||
},
|
||||
selectedLogLevel() {
|
||||
const decodedParams = decodeSearchParams(this.$route.query);
|
||||
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
||||
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
||||
return this.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
},
|
||||
endDate() {
|
||||
if (this.$route.query.endDate) {
|
||||
return this.$route.query.endDate;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
startDate() {
|
||||
// we mention the last refresh date here to trick
|
||||
// VueJs fine grained reactivity system and invalidate
|
||||
// computed property startDate
|
||||
if (this.$route.query.startDate && this.lastRefreshDate) {
|
||||
return this.$route.query.startDate;
|
||||
}
|
||||
if (this.$route.query.timeRange) {
|
||||
return this.$moment().subtract(this.$moment.duration(this.$route.query.timeRange).as("milliseconds")).toISOString(true);
|
||||
}
|
||||
const props = withDefaults(defineProps<{
|
||||
logLevel?: string;
|
||||
embed?: boolean;
|
||||
showFilters?: boolean;
|
||||
filters?: Record<string, any>;
|
||||
reloadLogs?: number;
|
||||
}>(), {
|
||||
embed: false,
|
||||
showFilters: false,
|
||||
filters: undefined,
|
||||
logLevel: undefined,
|
||||
reloadLogs: undefined
|
||||
});
|
||||
|
||||
// the default is PT30D
|
||||
return this.$moment().subtract(7, "days").toISOString(true);
|
||||
},
|
||||
namespace() {
|
||||
return this.$route.params.namespace ?? this.$route.params.id;
|
||||
},
|
||||
flowId() {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
charts() {
|
||||
return [
|
||||
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
||||
];
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to: any, _: any, next: (route?: any) => void) {
|
||||
const query = {...to.query};
|
||||
let queryHasChanged = false;
|
||||
const route = useRoute();
|
||||
const {t} = useI18n();
|
||||
const logsStore = useLogsStore();
|
||||
const logFilter = useLogFilter();
|
||||
|
||||
const queryKeys = Object.keys(query);
|
||||
if (defaultNamespace() && !queryKeys.some(key => key.startsWith("filters[namespace]"))) {
|
||||
query["filters[namespace][PREFIX]"] = defaultNamespace();
|
||||
queryHasChanged = true;
|
||||
}
|
||||
const routeInfo = computed(() => ({
|
||||
title: t("logs"),
|
||||
}));
|
||||
useRouteContext(routeInfo, props.embed);
|
||||
|
||||
if (queryHasChanged) {
|
||||
next({
|
||||
...to,
|
||||
query,
|
||||
replace: true
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showStatChart() {
|
||||
return this.showChart;
|
||||
},
|
||||
onShowChartChange(value: boolean) {
|
||||
this.showChart = value;
|
||||
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
||||
if (this.showStatChart()) {
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
this.lastRefreshDate = new Date();
|
||||
if (this.$refs.dashboard) {
|
||||
this.$refs.dashboard.refreshCharts();
|
||||
}
|
||||
this.load();
|
||||
},
|
||||
loadQuery(base: any) {
|
||||
let queryFilter = this.filters ?? this.queryWithFilter();
|
||||
const isLoading = ref(false);
|
||||
const lastRefreshDate = ref(new Date());
|
||||
const showChart = ref(localStorage.getItem(storageKeys.SHOW_LOGS_CHART) !== "false");
|
||||
const dashboardRef = ref();
|
||||
|
||||
if (this.isFlowEdit) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
||||
queryFilter["filters[flowId][EQUALS]"] = this.flowId;
|
||||
} else if (this.isNamespaceEdit) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = this.namespace;
|
||||
}
|
||||
const isFlowEdit = computed(() => route.name === "flows/update");
|
||||
const isNamespaceEdit = computed(() => route.name === "namespaces/update");
|
||||
const selectedLogLevel = computed(() => {
|
||||
const decodedParams = decodeSearchParams(route.query);
|
||||
const levelFilters = decodedParams.filter(item => item?.field === "level");
|
||||
const decoded = levelFilters.length > 0 ? levelFilters[0]?.value : "INFO";
|
||||
return props.logLevel || decoded || localStorage.getItem("defaultLogLevel") || "INFO";
|
||||
});
|
||||
const endDate = computed(() => {
|
||||
if (route.query.endDate) {
|
||||
return route.query.endDate;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const startDate = computed(() => {
|
||||
// we mention the last refresh date here to trick
|
||||
// VueJs fine grained reactivity system and invalidate
|
||||
// computed property startDate
|
||||
if (route.query.startDate && lastRefreshDate.value) {
|
||||
return route.query.startDate;
|
||||
}
|
||||
if (route.query.timeRange) {
|
||||
return moment().subtract(moment.duration(route.query.timeRange as string).as("milliseconds")).toISOString(true);
|
||||
}
|
||||
|
||||
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
||||
queryFilter["startDate"] = this.startDate;
|
||||
queryFilter["endDate"] = this.endDate;
|
||||
}
|
||||
// the default is PT30D
|
||||
return moment().subtract(7, "days").toISOString(true);
|
||||
});
|
||||
const flowId = computed(() => route.params.id);
|
||||
const namespace = computed(() => route.params.namespace ?? route.params.id);
|
||||
const charts = computed(() => [
|
||||
{...YAML_UTILS.parse(YAML_CHART), content: YAML_CHART}
|
||||
]);
|
||||
|
||||
delete queryFilter["level"];
|
||||
const loadQuery = (base: any) => {
|
||||
let queryFilter = props.filters ?? queryWithFilter();
|
||||
|
||||
return _merge(base, queryFilter)
|
||||
},
|
||||
load() {
|
||||
this.isLoading = true
|
||||
if (isFlowEdit.value) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||
queryFilter["filters[flowId][EQUALS]"] = flowId.value;
|
||||
} else if (isNamespaceEdit.value) {
|
||||
queryFilter["filters[namespace][EQUALS]"] = namespace.value;
|
||||
}
|
||||
|
||||
const data = {
|
||||
page: this.filters ? this.internalPageNumber : this.$route.query.page || this.internalPageNumber,
|
||||
size: this.filters ? this.internalPageSize : this.$route.query.size || this.internalPageSize,
|
||||
...this.filters
|
||||
};
|
||||
this.logsStore.findLogs(this.loadQuery({
|
||||
...data,
|
||||
minLevel: this.filters ? null : this.selectedLogLevel,
|
||||
sort: "timestamp:desc"
|
||||
}))
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
this.saveRestoreUrl();
|
||||
});
|
||||
if (!queryFilter["startDate"] || !queryFilter["endDate"]) {
|
||||
queryFilter["startDate"] = startDate.value;
|
||||
queryFilter["endDate"] = endDate.value;
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
reloadLogs(newValue) {
|
||||
if(newValue) this.refresh();
|
||||
},
|
||||
delete queryFilter["level"];
|
||||
|
||||
return _merge(base, queryFilter);
|
||||
};
|
||||
|
||||
const loadData = (callback?: () => void) => {
|
||||
isLoading.value = true;
|
||||
|
||||
const data = {
|
||||
page: props.filters ? internalPageNumber.value : route.query.page || internalPageNumber.value,
|
||||
size: props.filters ? internalPageSize.value : route.query.size || internalPageSize.value,
|
||||
...props.filters
|
||||
};
|
||||
|
||||
logsStore.findLogs(loadQuery({
|
||||
...data,
|
||||
minLevel: props.filters ? null : selectedLogLevel.value,
|
||||
sort: "timestamp:desc"
|
||||
}))
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
if (callback) callback();
|
||||
});
|
||||
};
|
||||
|
||||
const {onPageChanged, queryWithFilter, internalPageNumber, internalPageSize} = useDataTableActions({
|
||||
loadData
|
||||
});
|
||||
|
||||
const showStatChart = () => showChart.value;
|
||||
|
||||
const onShowChartChange = (value: boolean) => {
|
||||
showChart.value = value;
|
||||
localStorage.setItem(storageKeys.SHOW_LOGS_CHART, value.toString());
|
||||
if (showStatChart()) {
|
||||
loadData();
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
lastRefreshDate.value = new Date();
|
||||
if (dashboardRef.value) {
|
||||
dashboardRef.value.refreshCharts();
|
||||
}
|
||||
loadData();
|
||||
};
|
||||
|
||||
watch(() => route.query, () => {
|
||||
loadData();
|
||||
}, {deep: true});
|
||||
|
||||
watch(() => props.reloadLogs, (newValue) => {
|
||||
if (newValue) refresh();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Load data on mount if not embedded
|
||||
if (!props.embed) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onBeforeMount} from "vue";
|
||||
import {ref, computed, onBeforeMount, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {isEntryAPluginElementPredicate, TaskIcon} from "@kestra-io/ui-libs";
|
||||
import DottedLayout from "../layout/DottedLayout.vue";
|
||||
@@ -71,6 +71,7 @@
|
||||
import headerImage from "../../assets/icons/plugin.svg";
|
||||
import headerImageDark from "../../assets/icons/plugin-dark.svg";
|
||||
import {usePluginsStore} from "../../stores/plugins";
|
||||
import useRestoreUrl from "../../composables/useRestoreUrl";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -85,13 +86,23 @@
|
||||
embed: false
|
||||
});
|
||||
|
||||
const {saveRestoreUrl} = useRestoreUrl();
|
||||
|
||||
const icons = ref<Record<string, any>>({});
|
||||
const searchText = ref("");
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
searchText.value = query;
|
||||
const newQuery: Record<string, any> = {...route.query};
|
||||
if (query !== undefined && query !== null && String(query).trim() !== "") {
|
||||
newQuery.q = query;
|
||||
} else {
|
||||
// remove an empty `q=` in the URL on plugins/view
|
||||
delete newQuery.q;
|
||||
}
|
||||
|
||||
router.push({
|
||||
query: {...route.query, q: query || undefined}
|
||||
query: newQuery
|
||||
});
|
||||
};
|
||||
|
||||
@@ -177,6 +188,11 @@
|
||||
loadPluginIcons();
|
||||
searchText.value = String(route.query?.q ?? "");
|
||||
});
|
||||
|
||||
watch(() => route.query.q, (newQ) => {
|
||||
searchText.value = String(newQ ?? "");
|
||||
saveRestoreUrl();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {useRoute, useRouter} from "vue-router";
|
||||
import _merge from "lodash/merge";
|
||||
import _cloneDeep from "lodash/cloneDeep";
|
||||
import _isEqual from "lodash/isEqual";
|
||||
import useRestoreUrl from "./useRestoreUrl";
|
||||
|
||||
interface SortItem {
|
||||
prop?: string;
|
||||
@@ -26,7 +27,6 @@ interface DataTableActionsOptions {
|
||||
embed?: boolean;
|
||||
dataTableRef?: Ref<DataTableRef | null>;
|
||||
loadData?: (callback?: () => void) => void;
|
||||
saveRestoreUrl?: () => void;
|
||||
}
|
||||
|
||||
export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
@@ -35,7 +35,6 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
|
||||
const sort = ref("");
|
||||
const dblClickRouteName = ref(options.dblClickRouteName);
|
||||
const loadInit = ref(true);
|
||||
const ready = ref(false);
|
||||
const internalPageSize = ref(25);
|
||||
const internalPageNumber = ref(1);
|
||||
@@ -47,6 +46,8 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
const embed = computed(() => options.embed);
|
||||
const dataTableRef = computed(() => options.dataTableRef?.value);
|
||||
|
||||
const {loadInit, saveRestoreUrl} = useRestoreUrl({restoreUrl: true});
|
||||
|
||||
const sortString = (sortItem: SortItem, sortKeyMapper: (k: string) => string): string | undefined => {
|
||||
if (sortItem && sortItem.prop && sortItem.order) {
|
||||
return `${sortKeyMapper(sortItem.prop)}:${sortItem.order === "descending" ? "desc" : "asc"}`;
|
||||
@@ -149,9 +150,7 @@ export function useDataTableActions(options: DataTableActionsOptions = {}) {
|
||||
ready.value = true;
|
||||
loadInit.value = true;
|
||||
|
||||
if (options.saveRestoreUrl) {
|
||||
options.saveRestoreUrl();
|
||||
}
|
||||
saveRestoreUrl();
|
||||
|
||||
if (dataTableRef.value) {
|
||||
dataTableRef.value.isLoading = false;
|
||||
|
||||
@@ -47,6 +47,11 @@ export default function useRestoreUrl(options: UseRestoreUrlOptions = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges saved URL query parameters from sessionStorage with current route.
|
||||
* Only adds missing parameters to avoid overwriting user changes.
|
||||
* Updates route only when changes are made.
|
||||
*/
|
||||
const goToRestoreUrl = () => {
|
||||
if (!restoreUrl) {
|
||||
return;
|
||||
@@ -84,9 +89,12 @@ export default function useRestoreUrl(options: UseRestoreUrlOptions = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically call goToRestoreUrl on mount if needed (equivalent to created() hook)
|
||||
/**
|
||||
* Automatically restores saved URL state from sessionStorage on mount.
|
||||
* Only triggers when restoreUrl is enabled and saved state exists.
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (Object.keys(route.query).length === 0 && restoreUrl) {
|
||||
if (restoreUrl && localStorageValue.value) {
|
||||
loadInit.value = false;
|
||||
goToRestoreUrl();
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@
|
||||
import {useDocStore} from "../../../../stores/doc";
|
||||
import {canCreate} from "override/composables/blueprintsPermissions";
|
||||
import {useDataTableActions} from "../../../../composables/useDataTableActions";
|
||||
import useRestoreUrl from "../../../../composables/useRestoreUrl";
|
||||
import {useBlueprintFilter} from "../../../../components/filter/configurations";
|
||||
|
||||
const blueprintFilter = useBlueprintFilter();
|
||||
@@ -128,8 +127,6 @@
|
||||
|
||||
const {onPageChanged, onDataLoaded, load, ready, internalPageNumber, internalPageSize} = useDataTableActions({loadData});
|
||||
|
||||
useRestoreUrl();
|
||||
|
||||
const emit = defineEmits(["goToDetail", "loaded"]);
|
||||
|
||||
const route = useRoute();
|
||||
@@ -273,15 +270,13 @@
|
||||
docStore.docId = `blueprints.${props.blueprintType}`;
|
||||
});
|
||||
|
||||
watch(route,
|
||||
(newValue, oldValue) => {
|
||||
if (oldValue.name === newValue.name) {
|
||||
selectedTags.value = initSelectedTags();
|
||||
searchText.value = route.query.q || "";
|
||||
load(onDataLoaded);
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(route, (newRoute, oldRoute) => {
|
||||
if (newRoute.name === oldRoute.name) {
|
||||
selectedTags.value = initSelectedTags();
|
||||
searchText.value = newRoute.query.q || "";
|
||||
load(onDataLoaded);
|
||||
}
|
||||
});
|
||||
|
||||
watch(searchText, () => {
|
||||
load(onDataLoaded);
|
||||
|
||||
@@ -89,11 +89,14 @@
|
||||
import permission from "../../../models/permission";
|
||||
import action from "../../../models/action";
|
||||
|
||||
import useRestoreUrl from "../../../composables/useRestoreUrl";
|
||||
|
||||
import DotsSquare from "vue-material-design-icons/DotsSquare.vue";
|
||||
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
||||
import {useAuthStore} from "override/stores/auth";
|
||||
|
||||
const namespacesFilter = useNamespacesFilter();
|
||||
const {saveRestoreUrl} = useRestoreUrl({restoreUrl: true});
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
@@ -127,8 +130,12 @@
|
||||
|
||||
onMounted(() => loadData());
|
||||
watch(
|
||||
() => route.query,
|
||||
() => loadData(),
|
||||
() => route.query.q,
|
||||
() => {
|
||||
loadData();
|
||||
saveRestoreUrl();
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
const miscStore = useMiscStore();
|
||||
|
||||
@@ -7,21 +7,23 @@ import DemoAuditLogs from "../components/demo/AuditLogs.vue"
|
||||
import DemoInstance from "../components/demo/Instance.vue"
|
||||
import DemoApps from "../components/demo/Apps.vue"
|
||||
import DemoTests from "../components/demo/Tests.vue"
|
||||
import {useMiscStore} from "override/stores/misc";
|
||||
import {applyDefaultFilters} from "../components/filter/composables/useDefaultFilter";
|
||||
|
||||
function maybeAddTimeRangeFilter(to) {
|
||||
const dateTimeKeys = ["startDate", "endDate", "timeRange"];
|
||||
|
||||
// Default to the configured duration if no time range is set
|
||||
if (!Object.keys(to.query).some((key) => dateTimeKeys.some((dateTimeKey) => key.includes(dateTimeKey)))) {
|
||||
const miscStore = useMiscStore();
|
||||
const defaultDuration = miscStore.configs?.chartDefaultDuration || "P30D"; // Fallback to 30 days
|
||||
to.query["filters[timeRange][EQUALS]"] = defaultDuration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
export function applyBeforeEnterFilter(options) {
|
||||
return (to, _from, next) => {
|
||||
const {query, hasChanges} = applyDefaultFilters(to.query, options);
|
||||
|
||||
if (hasChanges) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
@@ -35,15 +37,6 @@ export default [
|
||||
path: "/:tenant?/dashboards/:dashboard?",
|
||||
component: () => import("../components/dashboard/Dashboard.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!to.params.dashboard) {
|
||||
next({
|
||||
name: "home",
|
||||
@@ -53,16 +46,21 @@ export default [
|
||||
},
|
||||
query: to.query,
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
applyBeforeEnterFilter({includeTimeRange: true, includeScope: false})(to, from, next);
|
||||
},
|
||||
},
|
||||
{name: "dashboards/create", path: "/:tenant?/dashboards/new", component: () => import("../components/dashboard/components/Create.vue")},
|
||||
{name: "dashboards/update", path: "/:tenant?/dashboards/:dashboard/edit", component: () => import("override/components/dashboard/Edit.vue")},
|
||||
|
||||
//Flows
|
||||
{name: "flows/list", path: "/:tenant?/flows", component: () => import("../components/flows/Flows.vue")},
|
||||
{
|
||||
name: "flows/list",
|
||||
path: "/:tenant?/flows",
|
||||
component: () => import("../components/flows/Flows.vue"),
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: false, includeScope: true}),
|
||||
},
|
||||
{name: "flows/search", path: "/:tenant?/flows/search", component: () => import("../components/flows/FlowsSearch.vue")},
|
||||
{name: "flows/create", path: "/:tenant?/flows/new", component: () => import("../components/flows/FlowCreate.vue")},
|
||||
{name: "flows/update", path: "/:tenant?/flows/edit/:namespace/:id/:tab?", component: () => import("../components/flows/FlowRoot.vue")},
|
||||
@@ -72,18 +70,7 @@ export default [
|
||||
name: "executions/list",
|
||||
path: "/:tenant?/executions",
|
||||
component: () => import("../components/executions/Executions.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: true, includeScope: true}),
|
||||
},
|
||||
{name: "executions/update", path: "/:tenant?/executions/:namespace/:flowId/:id/:tab?", component: () => import("../components/executions/ExecutionRoot.vue")},
|
||||
|
||||
@@ -111,18 +98,7 @@ export default [
|
||||
name: "logs/list",
|
||||
path: "/:tenant?/logs",
|
||||
component: () => import("../components/logs/LogsWrapper.vue"),
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (maybeAddTimeRangeFilter(to)) {
|
||||
next({
|
||||
name: to.name,
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
beforeEnter: applyBeforeEnterFilter({includeTimeRange: true, includeScope: false}),
|
||||
},
|
||||
|
||||
//Namespaces
|
||||
|
||||
Reference in New Issue
Block a user