diff --git a/web/src/components/dashboards/PanelSchemaRenderer.vue b/web/src/components/dashboards/PanelSchemaRenderer.vue index a8f3586e17..198ab3eccb 100644 --- a/web/src/components/dashboards/PanelSchemaRenderer.vue +++ b/web/src/components/dashboards/PanelSchemaRenderer.vue @@ -616,6 +616,74 @@ export default defineComponent({ tableRendererRef.value = null; parser = null; }); + const convertPanelDataCommon = async () => { + if ( + !errorDetail?.value?.message && + validatePanelData?.value?.length === 0 + ) { + try { + panelData.value = await convertPanelData( + panelSchema.value, + data.value, + store, + chartPanelRef, + hoveredSeriesState, + resultMetaData, + metadata.value, + chartPanelStyle.value, + annotations, + loading.value, + ); + + limitNumberOfSeriesWarningMessage.value = + panelData.value?.extras?.limitNumberOfSeriesWarningMessage ?? ""; + + errorDetail.value = { + message: "", + code: "", + }; + } catch (error: any) { + console.error("error", error); + errorDetail.value = { + message: error?.message, + code: error?.code || "", + }; + } + } else { + // if no data is available, then show the default data + // if there is an error config in the panel schema, then show the default data on error + // if no default data on error is set, then show the custom error message + if ( + panelSchema.value?.error_config?.custom_error_handeling && + panelSchema.value?.error_config?.default_data_on_error + ) { + data.value = JSON.parse( + panelSchema.value?.error_config?.default_data_on_error, + ); + errorDetail.value = { + message: "", + code: "", + }; + } + } + }; + + // Watch for panel schema changes to re-convert panel data + watch( + panelSchema, + async () => { + // Re-convert panel data when schema changes (for non-whitelisted config changes) + if ( + !errorDetail?.value?.message && + validatePanelData?.value?.length === 0 && + data.value?.length + ) { + await convertPanelDataCommon(); + } + }, + { deep: true }, + ); + watch( [data, store?.state], async () => { @@ -648,58 +716,8 @@ export default defineComponent({ code: "", }; - // panelData.value = convertPanelData(panelSchema.value, data.value, store); - if ( - !errorDetail?.value?.message && - validatePanelData?.value?.length === 0 - ) { - try { - // passing chartpanelref to get width and height of DOM element - panelData.value = await convertPanelData( - panelSchema.value, - data.value, - store, - chartPanelRef, - hoveredSeriesState, - resultMetaData, - metadata.value, - chartPanelStyle.value, - annotations, - loading.value, - ); - - limitNumberOfSeriesWarningMessage.value = - panelData.value?.extras?.limitNumberOfSeriesWarningMessage ?? ""; - - errorDetail.value = { - message: "", - code: "", - }; - } catch (error: any) { - console.error("error", error); - - errorDetail.value = { - message: error?.message, - code: error?.code || "", - }; - } - } else { - // if no data is available, then show the default data - // if there is an error config in the panel schema, then show the default data on error - // if no default data on error is set, then show the custom error message - if ( - panelSchema.value?.error_config?.custom_error_handeling && - panelSchema.value?.error_config?.default_data_on_error - ) { - data.value = JSON.parse( - panelSchema.value?.error_config?.default_data_on_error, - ); - errorDetail.value = { - message: "", - code: "", - }; - } - } + // Use the common function to convert panel data + await convertPanelDataCommon(); }, { deep: true }, ); diff --git a/web/src/components/dashboards/addPanel/DashboardQueryBuilder.vue b/web/src/components/dashboards/addPanel/DashboardQueryBuilder.vue index 2c90b55940..ab1be97e0b 100644 --- a/web/src/components/dashboards/addPanel/DashboardQueryBuilder.vue +++ b/web/src/components/dashboards/addPanel/DashboardQueryBuilder.vue @@ -27,6 +27,9 @@ along with this program. If not, see .
diff --git a/web/src/composables/dashboard/usePanelDataLoader.ts b/web/src/composables/dashboard/usePanelDataLoader.ts index 92fcee28df..2ca97afaff 100644 --- a/web/src/composables/dashboard/usePanelDataLoader.ts +++ b/web/src/composables/dashboard/usePanelDataLoader.ts @@ -45,6 +45,7 @@ import { convertOffsetToSeconds } from "@/utils/dashboard/convertDataIntoUnitVal import useSearchWebSocket from "@/composables/useSearchWebSocket"; import { useAnnotations } from "./useAnnotations"; import useHttpStreamingSearch from "../useStreamingSearch"; +import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/checkConfigChangeApiCall"; /** * debounce time in milliseconds for panel data loader @@ -1644,13 +1645,33 @@ export const usePanelDataLoader = ( watch( // Watching for changes in panelSchema, selectedTimeObj and forceLoad - () => [panelSchema?.value, selectedTimeObj?.value, forceLoad?.value], + () => [selectedTimeObj?.value, forceLoad?.value], async () => { log("PanelSchema/Time Wather: called"); loadData(); // Loading the data }, ); + watch( + () => [panelSchema?.value], + async (newVal, oldVal) => { + const [newSchema] = newVal; + const [oldSchema] = oldVal; + + const configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + oldSchema, + newSchema, + ); + + if (!configNeedsApiCall) { + return; + } + + loadData(); + }, + { deep: true }, + ); + /** * Replaces the query with the corresponding variable values. * diff --git a/web/src/composables/useDashboardPanel.ts b/web/src/composables/useDashboardPanel.ts index 689793a864..15f00d7556 100644 --- a/web/src/composables/useDashboardPanel.ts +++ b/web/src/composables/useDashboardPanel.ts @@ -1061,6 +1061,49 @@ const useDashboardPanelData = (pageKey: string = "dashboard") => { dashboardPanelData.data.customChartContent = getDefaultCustomChartText(); + dashboardPanelData.data.queries[ + dashboardPanelData.layout.currentQueryIndex + ].config.time_shift = []; + break; + case "metric": + dashboardPanelData.data.queries[ + dashboardPanelData.layout.currentQueryIndex + ].fields.x = []; + dashboardPanelData.data.queries[ + dashboardPanelData.layout.currentQueryIndex + ].fields.y.forEach((itemY: any) => { + if (itemY.aggregationFunction === null && !itemY.isDerived) { + itemY.aggregationFunction = "count"; + } + }); + dashboardPanelData.data.queries[ + dashboardPanelData.layout.currentQueryIndex + ].fields.z = []; + dashboardPanelData.data.queries[ + dashboardPanelData.layout.currentQueryIndex + ].fields.breakdown = []; + // we have multiple queries for geomap, so if we are moving away, we need to reset + // the values of lat, lng and weight in all the queries + dashboardPanelData.data.queries?.forEach((query: any) => { + query.fields.latitude = null; + query.fields.longitude = null; + query.fields.weight = null; + query.fields.name = null; + query.fields.value_for_maps = null; + query.fields.source = null; + query.fields.target = null; + query.fields.value = null; + }); + if (dashboardPanelData.data.queryType === "sql") { + dashboardPanelData.layout.currentQueryIndex = 0; + dashboardPanelData.data.queries = + dashboardPanelData.data.queries.slice(0, 1); + } + dashboardPanelData.data.htmlContent = ""; + dashboardPanelData.data.markdownContent = ""; + dashboardPanelData.data.customChartContent = + getDefaultCustomChartText(); + dashboardPanelData.data.queries[ dashboardPanelData.layout.currentQueryIndex ].config.time_shift = []; @@ -2035,7 +2078,8 @@ const useDashboardPanelData = (pageKey: string = "dashboard") => { const { sqlParser }: any = useSqlParser.default(); parser = await sqlParser(); - updateQueryValue(); + // do not allow to modify custom query fields for logs page + updateQueryValue(pageKey == "logs" ? true : false); }; /** diff --git a/web/src/plugins/logs/Index.vue b/web/src/plugins/logs/Index.vue index 45ec40c28e..4bf78e1e0b 100644 --- a/web/src/plugins/logs/Index.vue +++ b/web/src/plugins/logs/Index.vue @@ -389,6 +389,7 @@ import { getFieldsFromQuery, } from "@/utils/query/sqlUtils"; import useNotifications from "@/composables/useNotifications"; +import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/checkConfigChangeApiCall"; import SearchBar from "@/plugins/logs/SearchBar.vue"; import SearchHistory from "@/plugins/logs/SearchHistory.vue"; import SearchSchedulersList from "@/plugins/logs/SearchSchedulersList.vue"; @@ -1666,6 +1667,26 @@ export default defineComponent({ }, ); + // Auto-apply config changes that don't require API calls (similar to dashboard) + const debouncedUpdateChartConfig = debounce((newVal) => { + if (searchObj.meta.logsVisualizeToggle === "visualize") { + + let configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + visualizeChartData.value, + newVal, + ); + + if (!configNeedsApiCall) { + visualizeChartData.value = JSON.parse(JSON.stringify(newVal)); + window.dispatchEvent(new Event("resize")); + } + } + }, 1000); + + watch(() => dashboardPanelData.data, debouncedUpdateChartConfig, { + deep: true, + }); + watch( () => [ searchObj.data.datetime.type, diff --git a/web/src/plugins/logs/VisualizeLogsQuery.vue b/web/src/plugins/logs/VisualizeLogsQuery.vue index 6ff9d52f83..a6d60de866 100644 --- a/web/src/plugins/logs/VisualizeLogsQuery.vue +++ b/web/src/plugins/logs/VisualizeLogsQuery.vue @@ -404,6 +404,7 @@ import { isEqual } from "lodash-es"; import { onActivated } from "vue"; import useNotifications from "@/composables/useNotifications"; import CustomChartEditor from "@/components/dashboards/addPanel/CustomChartEditor.vue"; +import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/checkConfigChangeApiCall"; const ConfigPanel = defineAsyncComponent(() => { return import("@/components/dashboards/addPanel/ConfigPanel.vue"); @@ -466,6 +467,7 @@ export default defineComponent({ const { dashboardPanelData, resetAggregationFunction, validatePanel } = useDashboardPanelData(dashboardPanelDataPageKey); const metaData = ref(null); + const resultMetaData = ref(null); const seriesData = ref([] as any[]); const seriesDataUpdate = (data: any) => { seriesData.value = data; @@ -492,12 +494,17 @@ export default defineComponent({ ); const isOutDated = computed(() => { - //compare chartdata and dashboardpaneldata - // ignore histogram query comparison - return ( - !is_ui_histogram.value && - !isEqual(chartData.value, dashboardPanelData.data) - ); + const configChanged = !isEqual(chartData.value, dashboardPanelData.data); + + // skip outdate check if doesnt require to call api + let configNeedsApiCall = false; + if (configChanged) { + configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + chartData.value, + dashboardPanelData.data, + ); + } + return configNeedsApiCall; }); watch(isOutDated, () => { @@ -569,6 +576,13 @@ export default defineComponent({ provide("hoveredSeriesState", hoveredSeriesState); const addToDashboard = () => { + + // only copy if is_ui_histogram is true + if (resultMetaData.value?.[0]?.converted_histogram_query && is_ui_histogram.value === true) { + dashboardPanelData.data.queries[0].query = + resultMetaData.value?.[0]?.converted_histogram_query; + } + const errors: any = []; // will push errors in errors array validatePanel(errors, true); @@ -581,6 +595,7 @@ export default defineComponent({ ); return; } else { + showAddToDashboardDialog.value = true; } }; @@ -767,15 +782,15 @@ export default defineComponent({ window.dispatchEvent(new Event("resize")); }; - const onResultMetadataUpdate = (resultMetaData: any) => { + const onResultMetadataUpdate = (resultMetaDataParam: any) => { + // Store the result metadata + resultMetaData.value = resultMetaDataParam; + // only copy if is_ui_histogram is true - if ( - resultMetaData?.[0]?.converted_histogram_query && - is_ui_histogram.value === true - ) { - dashboardPanelData.data.queries[0].query = - resultMetaData?.[0]?.converted_histogram_query; - } + // if (resultMetaDataParam?.[0]?.converted_histogram_query && is_ui_histogram.value === true) { + // dashboardPanelData.data.queries[0].query = + // resultMetaDataParam?.[0]?.converted_histogram_query; + // } }; return { diff --git a/web/src/plugins/metrics/Index.vue b/web/src/plugins/metrics/Index.vue index 3fd2c874b2..b52083aa62 100644 --- a/web/src/plugins/metrics/Index.vue +++ b/web/src/plugins/metrics/Index.vue @@ -522,12 +522,13 @@ import DashboardErrorsComponent from "@/components/dashboards/addPanel/Dashboard import VariablesValueSelector from "@/components/dashboards/VariablesValueSelector.vue"; import PanelSchemaRenderer from "@/components/dashboards/PanelSchemaRenderer.vue"; import RelativeTime from "@/components/common/RelativeTime.vue"; -import { isEqual } from "lodash-es"; +import { isEqual, debounce } from "lodash-es"; import { provide } from "vue"; import useNotifications from "@/composables/useNotifications"; import config from "@/aws-exports"; import useCancelQuery from "@/composables/dashboard/useCancelQuery"; import AutoRefreshInterval from "@/components/AutoRefreshInterval.vue"; +import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/checkConfigChangeApiCall"; import CustomChartEditor from "@/components/dashboards/addPanel/CustomChartEditor.vue"; const AddToDashboard = defineAsyncComponent(() => { return import("./../metrics/AddToDashboard.vue"); @@ -779,6 +780,25 @@ export default defineComponent({ { deep: true }, ); + // Auto-apply config changes that don't require API calls (similar to dashboard) + const debouncedUpdateChartConfig = debounce((newVal, oldVal) => { + if (!isEqual(chartData.value, newVal)) { + const configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + chartData.value, + newVal, + ); + + if (!configNeedsApiCall) { + chartData.value = JSON.parse(JSON.stringify(newVal)); + window.dispatchEvent(new Event("resize")); + } + } + }, 1000); + + watch(() => dashboardPanelData.data, debouncedUpdateChartConfig, { + deep: true, + }); + //validate the data const isValid = (onlyChart = false, isFieldsValidationRequired = true) => { const errors = errorData.errors; diff --git a/web/src/utils/dashboard/checkConfigChangeApiCall.ts b/web/src/utils/dashboard/checkConfigChangeApiCall.ts new file mode 100644 index 0000000000..c36c18779a --- /dev/null +++ b/web/src/utils/dashboard/checkConfigChangeApiCall.ts @@ -0,0 +1,65 @@ +// Whitelist of config keys that require API calls when changed +const API_REQUIRING_CHANGES = new Set(["queries", "variables"]); + +/** + * Flattens an object's keys with dot notation + */ +const flattenObject = (obj: any, prefix = ""): Record => { + if (obj === null || obj === undefined) { + return {}; + } + return Object.keys(obj).reduce((acc: Record, k: string) => { + const pre = prefix.length ? prefix + "." : ""; + if ( + typeof obj[k] === "object" && + obj[k] !== null && + !Array.isArray(obj[k]) + ) { + Object.assign(acc, flattenObject(obj[k], pre + k)); + } else { + acc[pre + k] = obj[k]; + } + return acc; + }, {}); +}; + +/** + * Gets changed keys between two objects + */ +const getChangedKeys = (oldObj: any, newObj: any): string[] => { + const flatOld = flattenObject(oldObj); + const flatNew = flattenObject(newObj); + const changedKeys: string[] = []; + + // Check all keys in both objects + const allKeys = Array.from( + new Set([...Object.keys(flatOld), ...Object.keys(flatNew)]), + ); + + for (const key of allKeys) { + if (JSON.stringify(flatOld[key]) !== JSON.stringify(flatNew[key])) { + changedKeys.push(key); + } + } + + return changedKeys; +}; + +/** + * Checks if the changes between old and new config require an API call + */ +export const checkIfConfigChangeRequiredApiCallOrNot = ( + oldConfig: any, + newConfig: any, +): boolean => { + const changedKeys = getChangedKeys(oldConfig, newConfig); + const configNeedsApiCall = changedKeys.some((key) => + Array.from(API_REQUIRING_CHANGES).some((apiKey) => key.startsWith(apiKey)), + ); + + if (configNeedsApiCall) { + return true; + } + + return false; +}; diff --git a/web/src/views/Dashboards/addPanel/AddPanel.vue b/web/src/views/Dashboards/addPanel/AddPanel.vue index ebc9d3c66d..ae77d224d7 100644 --- a/web/src/views/Dashboards/addPanel/AddPanel.vue +++ b/web/src/views/Dashboards/addPanel/AddPanel.vue @@ -510,13 +510,14 @@ import VariablesValueSelector from "../../../components/dashboards/VariablesValu import PanelSchemaRenderer from "../../../components/dashboards/PanelSchemaRenderer.vue"; import RelativeTime from "@/components/common/RelativeTime.vue"; import { useLoading } from "@/composables/useLoading"; -import { isEqual } from "lodash-es"; +import { debounce, isEqual } from "lodash-es"; import { provide } from "vue"; import useNotifications from "@/composables/useNotifications"; import config from "@/aws-exports"; import useCancelQuery from "@/composables/dashboard/useCancelQuery"; import useAiChat from "@/composables/useAiChat"; import useStreams from "@/composables/useStreams"; +import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/checkConfigChangeApiCall"; const ConfigPanel = defineAsyncComponent(() => { return import("../../../components/dashboards/addPanel/ConfigPanel.vue"); @@ -899,11 +900,19 @@ export default defineComponent({ const normalizedCurrent = normalizeVariables(variablesData); const normalizedRefreshed = normalizeVariables(updatedVariablesData); + const variablesChanged = !isEqual(normalizedCurrent, normalizedRefreshed); - return ( - !isEqual(chartData.value, dashboardPanelData.data) || - !isEqual(normalizedCurrent, normalizedRefreshed) - ); + const configChanged = !isEqual(chartData.value, dashboardPanelData.data); + let configNeedsApiCall = false; + + if (configChanged) { + configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + chartData.value, + dashboardPanelData.data, + ); + } + + return configNeedsApiCall || variablesChanged; }); watch(isOutDated, () => { @@ -1561,6 +1570,24 @@ export default defineComponent({ return { width: `${contentWidth}px` }; }); + const debouncedUpdateChartConfig = debounce((newVal, oldVal) => { + if (!isEqual(chartData.value, newVal)) { + const configNeedsApiCall = checkIfConfigChangeRequiredApiCallOrNot( + chartData.value, + newVal, + ); + + if (!configNeedsApiCall) { + chartData.value = JSON.parse(JSON.stringify(newVal)); + + window.dispatchEvent(new Event("resize")); + } + } + }, 1000); + + watch(() => dashboardPanelData.data, debouncedUpdateChartConfig, { + deep: true, + }); // [START] ai chat // [START] O2 AI Context Handler