mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
feat: metrics page share url support (#12607)
Design At: https://github.com/openobserve/designs/blob/main/metrics-page/share-url/version2
This commit is contained in:
@@ -34,7 +34,11 @@ vi.mock("vuex", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("vue-router", () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(() => Promise.resolve()),
|
||||
}),
|
||||
useRoute: () => ({ query: {}, fullPath: "/metrics" }),
|
||||
}));
|
||||
|
||||
const mockShowErrorNotification = vi.fn();
|
||||
@@ -139,6 +143,9 @@ const createWrapper = (props: Record<string, any> = {}) => {
|
||||
props,
|
||||
global: {
|
||||
stubs: {
|
||||
// ShareButton pulls in the vuex store (store.commit) + short_url; the
|
||||
// metrics page tests don't exercise it (it has its own spec).
|
||||
ShareButton: true,
|
||||
// Heavy async / complex children
|
||||
PanelEditor: {
|
||||
template:
|
||||
|
||||
@@ -52,6 +52,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
class="dashboard-icons"
|
||||
data-test="metrics-auto-refresh"
|
||||
/>
|
||||
<ShareButton
|
||||
v-if="!['html', 'markdown'].includes(dashboardPanelData.data.type)"
|
||||
:url="metricsShareUrl"
|
||||
variant="outline"
|
||||
size="icon-toolbar"
|
||||
data-test="metrics-share-btn"
|
||||
class="dashboard-icons"
|
||||
/>
|
||||
<template
|
||||
v-if="!['html', 'markdown'].includes(dashboardPanelData.data.type)"
|
||||
>
|
||||
@@ -134,6 +142,22 @@ import { checkIfConfigChangeRequiredApiCallOrNot } from "@/utils/dashboard/check
|
||||
import { PanelEditor } from "@/components/dashboards/PanelEditor";
|
||||
import { saveMetricsStream, restoreMetricsStream } from "@/utils/streamPersist";
|
||||
import useDefaultPanelFields from "@/composables/dashboard/useDefaultPanelFields";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import ShareButton from "@/components/common/ShareButton.vue";
|
||||
import {
|
||||
getMetricsConfig,
|
||||
encodeMetricsConfig,
|
||||
applyMetricsBlob,
|
||||
applyDeepLinkOverrides,
|
||||
} from "@/composables/metrics/metricsUrlState";
|
||||
import {
|
||||
queryParamsToSelectedDate,
|
||||
selectedDateToQueryParams,
|
||||
refreshLabelToInterval,
|
||||
refreshIntervalToLabel,
|
||||
} from "@/utils/dashboard/urlTimeParams";
|
||||
import { hasAnyDeepLinkParam } from "@/utils/url/deepLinkParams";
|
||||
import { METRICS_PARAMS } from "@/utils/metrics/metricsParamRegistry";
|
||||
|
||||
const AddToDashboard = defineAsyncComponent(() => {
|
||||
return import("./../metrics/AddToDashboard.vue");
|
||||
@@ -153,6 +177,7 @@ export default defineComponent({
|
||||
AutoRefreshInterval,
|
||||
PanelEditor,
|
||||
OButton,
|
||||
ShareButton,
|
||||
},
|
||||
setup(props) {
|
||||
provide("dashboardPanelDataPageKey", "metrics");
|
||||
@@ -165,6 +190,8 @@ export default defineComponent({
|
||||
const chartData = ref();
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { showErrorNotification } = useNotifications();
|
||||
const {
|
||||
dashboardPanelData,
|
||||
@@ -194,25 +221,10 @@ export default defineComponent({
|
||||
data: {},
|
||||
});
|
||||
|
||||
// this is used to activate the watcher only after on mounted
|
||||
let isPanelConfigWatcherActivated = false;
|
||||
const isPanelConfigChanged = ref(false);
|
||||
// Not kept-alive: re-mounts each navigation, so onMounted is the only restore point.
|
||||
let pendingAutoRun = false;
|
||||
|
||||
onUnmounted(() => {
|
||||
// NOTE: Do NOT call resetDashboardPanelData() here.
|
||||
// When org changes, Vue mounts the new component (onBeforeMount) BEFORE
|
||||
// unmounting the old one (onUnmounted). Resetting the shared singleton
|
||||
// dashboardPanelDataObj["metrics"] here would overwrite the "promql" state
|
||||
// that the new instance just set, causing the PromQL query type to
|
||||
// disappear after every org switch. The new instance's onBeforeMount
|
||||
// already resets and re-initialises the state correctly.
|
||||
});
|
||||
|
||||
// Initialize state before any child components mount so FieldList.vue sees
|
||||
// stream_type = "metrics" from the start, preventing a spurious
|
||||
// streams?type=logs request and the double stream-list fetch that results
|
||||
// from stream_type changing logs → metrics after children have mounted.
|
||||
onBeforeMount(() => {
|
||||
const applyMetricsDefaults = () => {
|
||||
errorData.errors = [];
|
||||
editMode.value = false;
|
||||
resetDashboardPanelData();
|
||||
@@ -229,16 +241,98 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// set default chart type as line
|
||||
dashboardPanelData.data.type = "line";
|
||||
// set the default query type as promql for metrics
|
||||
dashboardPanelData.data.queryType = "promql";
|
||||
dashboardPanelData.data.queries[0].customQuery = false;
|
||||
|
||||
// set the show query bar by default for metrics page
|
||||
dashboardPanelData.layout.showQueryBar = true;
|
||||
|
||||
chartData.value = {};
|
||||
};
|
||||
|
||||
// panel -> URL: fresh metrics_data blob + time/refresh, dropping override params (diff-guarded).
|
||||
const syncStateToUrl = () => {
|
||||
const query: Record<string, any> = {
|
||||
org_identifier: store.state.selectedOrganization.identifier,
|
||||
refresh: refreshIntervalToLabel(refreshInterval.value),
|
||||
...selectedDateToQueryParams(selectedDate.value),
|
||||
metrics_data: encodeMetricsConfig(getMetricsConfig(dashboardPanelData)),
|
||||
};
|
||||
const changed =
|
||||
Object.keys(query).some(
|
||||
(k) => String(query[k]) !== String(route.query[k] ?? ""),
|
||||
) || Object.keys(route.query).some((k) => !(k in query));
|
||||
if (changed) router.replace({ query }).catch(() => {});
|
||||
};
|
||||
|
||||
// URL -> panel (blob -> overrides -> time/refresh); returns the auto-run gate.
|
||||
const hydrateFromUrl = (): boolean => {
|
||||
const q = route.query as Record<string, any>;
|
||||
|
||||
if (q.metrics_data) applyMetricsBlob(q.metrics_data, dashboardPanelData);
|
||||
applyDeepLinkOverrides(q, dashboardPanelData);
|
||||
|
||||
if (q.period || (q.from && q.to)) {
|
||||
selectedDate.value = queryParamsToSelectedDate(q);
|
||||
}
|
||||
if (q.refresh != null) {
|
||||
refreshInterval.value = refreshLabelToInterval(
|
||||
q.refresh,
|
||||
store.state?.zoConfig?.min_auto_refresh_interval || 0,
|
||||
);
|
||||
}
|
||||
|
||||
return !!q.metrics_data || hasAnyDeepLinkParam(q, METRICS_PARAMS);
|
||||
};
|
||||
|
||||
// seed builder-mode slots (a stream but no query) with a starter query before auto-run.
|
||||
const seedBuilderSlots = async () => {
|
||||
const queries = dashboardPanelData.data.queries;
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
const qq = queries[i];
|
||||
if (qq?.fields?.stream && !qq.customQuery && !qq.query) {
|
||||
dashboardPanelData.layout.currentQueryIndex = i;
|
||||
await applyDefaultPanelFields();
|
||||
}
|
||||
}
|
||||
dashboardPanelData.layout.currentQueryIndex = 0;
|
||||
};
|
||||
|
||||
// Share URL: fresh /metrics link from current editor state, freezing relative period to absolute.
|
||||
const metricsShareUrl = computed(() => {
|
||||
void route.fullPath; // reactive dep on URL
|
||||
const url = new URL(window.location.origin + window.location.pathname);
|
||||
const sp = url.searchParams;
|
||||
sp.set("org_identifier", store.state.selectedOrganization.identifier);
|
||||
|
||||
// freeze the window: getConsumableDateTime() is in MICROSECONDS -> ms
|
||||
const ct: any = dateTimePickerRef.value?.getConsumableDateTime?.();
|
||||
if (ct?.startTime && ct?.endTime) {
|
||||
sp.set("from", String(Math.floor(ct.startTime / 1000)));
|
||||
sp.set("to", String(Math.floor(ct.endTime / 1000)));
|
||||
} else {
|
||||
const tp: any = selectedDateToQueryParams(selectedDate.value);
|
||||
if (tp.period) sp.set("period", tp.period);
|
||||
else if (tp.from != null) {
|
||||
sp.set("from", String(tp.from));
|
||||
sp.set("to", String(tp.to));
|
||||
}
|
||||
}
|
||||
|
||||
sp.set("refresh", refreshIntervalToLabel(refreshInterval.value));
|
||||
sp.set(
|
||||
"metrics_data",
|
||||
encodeMetricsConfig(getMetricsConfig(dashboardPanelData)),
|
||||
);
|
||||
return url.href;
|
||||
});
|
||||
|
||||
// this is used to activate the watcher only after on mounted
|
||||
let isPanelConfigWatcherActivated = false;
|
||||
const isPanelConfigChanged = ref(false);
|
||||
|
||||
// Reset before children mount so FieldList sees stream_type=metrics (avoids a spurious logs stream fetch).
|
||||
onBeforeMount(() => {
|
||||
applyMetricsDefaults();
|
||||
pendingAutoRun = hydrateFromUrl();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -249,15 +343,13 @@ export default defineComponent({
|
||||
await nextTick();
|
||||
isPanelConfigWatcherActivated = true;
|
||||
|
||||
// Seed the default builder query on initial load (the PromQL builder only
|
||||
// rebuilds on label/operation change, so the bar would otherwise be empty).
|
||||
const initialQuery = dashboardPanelData.data.queries[0];
|
||||
if (
|
||||
initialQuery?.fields?.stream &&
|
||||
!initialQuery.customQuery &&
|
||||
!initialQuery.query
|
||||
) {
|
||||
await applyDefaultPanelFields();
|
||||
await seedBuilderSlots();
|
||||
|
||||
// auto-run a restored blob / inbound deep-link, then normalize the URL
|
||||
if (pendingAutoRun) {
|
||||
pendingAutoRun = false;
|
||||
updateDateTime(selectedDate.value);
|
||||
runQuery();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -266,6 +358,11 @@ export default defineComponent({
|
||||
async () => {
|
||||
await nextTick();
|
||||
chartData.value = JSON.parse(JSON.stringify(dashboardPanelData.data));
|
||||
|
||||
// chart-type change re-renders outside runQuery(); re-sync only for an established view (metrics_data present).
|
||||
if (isPanelConfigWatcherActivated && route.query.metrics_data) {
|
||||
syncStateToUrl();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -332,6 +429,9 @@ export default defineComponent({
|
||||
if (panelEditorRef.value) {
|
||||
panelEditorRef.value.runQuery();
|
||||
}
|
||||
|
||||
// panel -> URL (full blob + time/refresh); normalizes any inbound params.
|
||||
syncStateToUrl();
|
||||
};
|
||||
|
||||
const updateDateTime = (value: object) => {
|
||||
@@ -522,6 +622,7 @@ export default defineComponent({
|
||||
addToDashboard,
|
||||
refreshInterval,
|
||||
panelEditorRef,
|
||||
metricsShareUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user