Compare commits

...

1 Commits

Author SHA1 Message Date
Bart Ledoux
fc8a9fe6cf Reapply "fix(core): bring the usage of restore url (#12762)" (#12915)
This reverts commit 8a7f2938b1.
2025-11-12 16:35:46 +01:00
16 changed files with 305 additions and 326 deletions

View File

@@ -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 {

View File

@@ -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) => {

View 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});
}
});
}

View File

@@ -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 {

View File

@@ -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: [],
};

View File

@@ -3,7 +3,6 @@
:namespace="flowStore.flow?.namespace"
:flowId="flowStore.flow?.id"
:topbar="false"
:restoreUrl="false"
filter
/>
</template>

View File

@@ -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">

View File

@@ -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({

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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">

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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