2020-10-22 16:31:58 +08:00
|
|
|
import memoizeOne from 'memoize-one';
|
2022-04-22 21:33:13 +08:00
|
|
|
|
2024-12-02 21:57:25 +08:00
|
|
|
import { AbsoluteTimeRange, LogRowModel, UrlQueryMap } from '@grafana/data';
|
2025-06-12 17:03:52 +08:00
|
|
|
import { t } from '@grafana/i18n';
|
2024-05-03 23:02:18 +08:00
|
|
|
import { getBackendSrv, config, locationService } from '@grafana/runtime';
|
|
|
|
import { sceneGraph, SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
2021-03-15 22:11:52 +08:00
|
|
|
import { notifyApp } from 'app/core/actions';
|
|
|
|
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
|
2024-05-03 23:02:18 +08:00
|
|
|
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
2025-01-09 06:58:54 +08:00
|
|
|
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
|
2022-04-22 21:33:13 +08:00
|
|
|
import { dispatch } from 'app/store/store';
|
|
|
|
|
2025-09-13 07:23:50 +08:00
|
|
|
import { ShortURL } from '../../../../apps/shorturl/plugin/src/generated/shorturl/v1alpha1/shorturl_object_gen';
|
|
|
|
import { BASE_URL as k8sShortURLBaseAPI } from '../../api/clients/shorturl/v1alpha1/baseAPI';
|
2024-06-25 03:31:42 +08:00
|
|
|
import { ShareLinkConfiguration } from '../../features/dashboard-scene/sharing/ShareButton/utils';
|
|
|
|
|
2022-04-22 21:33:13 +08:00
|
|
|
import { copyStringToClipboard } from './explore';
|
2020-10-22 16:31:58 +08:00
|
|
|
|
|
|
|
function buildHostUrl() {
|
|
|
|
return `${window.location.protocol}//${window.location.host}${config.appSubUrl}`;
|
|
|
|
}
|
|
|
|
|
2025-09-13 07:23:50 +08:00
|
|
|
export function buildShortUrl(k8sShortUrl: ShortURL) {
|
|
|
|
const key = k8sShortUrl.metadata.name;
|
|
|
|
const orgId = k8sShortUrl.metadata.namespace;
|
|
|
|
const hostUrl = buildHostUrl();
|
|
|
|
return `${hostUrl}/goto/${key}?orgId=${orgId}`;
|
|
|
|
}
|
|
|
|
|
2020-10-22 16:31:58 +08:00
|
|
|
function getRelativeURLPath(url: string) {
|
|
|
|
let path = url.replace(buildHostUrl(), '');
|
|
|
|
return path.startsWith('/') ? path.substring(1, path.length) : path;
|
|
|
|
}
|
|
|
|
|
2021-01-20 14:59:48 +08:00
|
|
|
export const createShortLink = memoizeOne(async function (path: string) {
|
2020-10-22 16:31:58 +08:00
|
|
|
try {
|
2025-09-13 07:23:50 +08:00
|
|
|
if (config.featureToggles.useKubernetesShortURLsAPI) {
|
|
|
|
// TODO: this is not ideal, we should use the RTK API but we can't call a hook from here and
|
|
|
|
// this util function is being called from several places, will require a bigger refactor including some code that
|
|
|
|
// is deprecated.
|
|
|
|
const k8sShortUrl: ShortURL = await getBackendSrv().post(`${k8sShortURLBaseAPI}/shorturls`, {
|
|
|
|
spec: {
|
|
|
|
path: getRelativeURLPath(path),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return buildShortUrl(k8sShortUrl);
|
|
|
|
} else {
|
|
|
|
// Old short URL API
|
|
|
|
const shortLink = await getBackendSrv().post(`/api/short-urls`, {
|
|
|
|
path: getRelativeURLPath(path),
|
|
|
|
});
|
|
|
|
return shortLink.url;
|
|
|
|
}
|
2020-10-22 16:31:58 +08:00
|
|
|
} catch (err) {
|
|
|
|
console.error('Error when creating shortened link: ', err);
|
2021-03-15 22:11:52 +08:00
|
|
|
dispatch(notifyApp(createErrorNotification('Error generating shortened link')));
|
2020-10-22 16:31:58 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-07-11 00:59:41 +08:00
|
|
|
/**
|
|
|
|
* Creates a ClipboardItem for the shortened link. This is used due to clipboard issues in Safari after making async calls.
|
|
|
|
* See https://github.com/grafana/grafana/issues/106889
|
|
|
|
* @param path - The long path to share.
|
|
|
|
* @returns A ClipboardItem for the shortened link.
|
|
|
|
*/
|
|
|
|
const createShortLinkClipboardItem = (path: string) => {
|
|
|
|
return new ClipboardItem({
|
|
|
|
'text/plain': createShortLink(path),
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-10-22 16:31:58 +08:00
|
|
|
export const createAndCopyShortLink = async (path: string) => {
|
2025-07-11 00:59:41 +08:00
|
|
|
if (typeof ClipboardItem !== 'undefined' && navigator.clipboard.write) {
|
|
|
|
navigator.clipboard.write([createShortLinkClipboardItem(path)]);
|
2021-03-15 22:11:52 +08:00
|
|
|
dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard')));
|
2020-10-22 16:31:58 +08:00
|
|
|
} else {
|
2025-07-11 00:59:41 +08:00
|
|
|
const shortLink = await createShortLink(path);
|
|
|
|
if (shortLink) {
|
|
|
|
copyStringToClipboard(shortLink);
|
|
|
|
dispatch(notifyApp(createSuccessNotification('Shortened link copied to clipboard')));
|
|
|
|
} else {
|
|
|
|
dispatch(notifyApp(createErrorNotification('Error generating shortened link')));
|
|
|
|
}
|
2020-10-22 16:31:58 +08:00
|
|
|
}
|
|
|
|
};
|
2024-05-03 23:02:18 +08:00
|
|
|
|
2024-06-25 03:31:42 +08:00
|
|
|
export const createAndCopyShareDashboardLink = async (
|
2024-05-03 23:02:18 +08:00
|
|
|
dashboard: DashboardScene,
|
2024-06-25 03:31:42 +08:00
|
|
|
opts: ShareLinkConfiguration,
|
2024-05-03 23:02:18 +08:00
|
|
|
panel?: VizPanel
|
|
|
|
) => {
|
2024-06-25 03:31:42 +08:00
|
|
|
const shareUrl = createDashboardShareUrl(dashboard, opts, panel);
|
|
|
|
if (opts.useShortUrl) {
|
|
|
|
return await createAndCopyShortLink(shareUrl);
|
|
|
|
} else {
|
|
|
|
copyStringToClipboard(shareUrl);
|
|
|
|
dispatch(notifyApp(createSuccessNotification(t('link.share.copy-to-clipboard', 'Link copied to clipboard'))));
|
|
|
|
}
|
2024-05-03 23:02:18 +08:00
|
|
|
};
|
|
|
|
|
2024-06-25 03:31:42 +08:00
|
|
|
export const createDashboardShareUrl = (dashboard: DashboardScene, opts: ShareLinkConfiguration, panel?: VizPanel) => {
|
2024-05-03 23:02:18 +08:00
|
|
|
const location = locationService.getLocation();
|
|
|
|
const timeRange = sceneGraph.getTimeRange(panel ?? dashboard);
|
|
|
|
|
|
|
|
const urlParamsUpdate = getShareUrlParams(opts, timeRange, panel);
|
|
|
|
|
|
|
|
return getDashboardUrl({
|
|
|
|
uid: dashboard.state.uid,
|
|
|
|
slug: dashboard.state.meta.slug,
|
|
|
|
currentQueryParams: location.search,
|
|
|
|
updateQuery: urlParamsUpdate,
|
2025-02-04 21:00:55 +08:00
|
|
|
absolute: !opts.useShortUrl,
|
2024-05-03 23:02:18 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getShareUrlParams = (
|
|
|
|
opts: { useAbsoluteTimeRange: boolean; theme: string },
|
|
|
|
timeRange: SceneTimeRangeLike,
|
|
|
|
panel?: VizPanel
|
|
|
|
) => {
|
|
|
|
const urlParamsUpdate: UrlQueryMap = {};
|
|
|
|
|
|
|
|
if (panel) {
|
2025-08-20 16:21:18 +08:00
|
|
|
urlParamsUpdate.viewPanel = panel.getPathId();
|
2024-05-03 23:02:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.useAbsoluteTimeRange) {
|
|
|
|
urlParamsUpdate.from = timeRange.state.value.from.toISOString();
|
|
|
|
urlParamsUpdate.to = timeRange.state.value.to.toISOString();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (opts.theme !== 'current') {
|
|
|
|
urlParamsUpdate.theme = opts.theme;
|
|
|
|
}
|
|
|
|
|
|
|
|
return urlParamsUpdate;
|
|
|
|
};
|
2024-12-02 21:57:25 +08:00
|
|
|
|
|
|
|
function getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel | null {
|
|
|
|
for (let i = allLogs.indexOf(row) - 1; i >= 0; i--) {
|
|
|
|
if (allLogs[i].timeEpochMs > row.timeEpochMs) {
|
|
|
|
return allLogs[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getLogsPermalinkRange(row: LogRowModel, rows: LogRowModel[], absoluteRange: AbsoluteTimeRange) {
|
|
|
|
const range = {
|
|
|
|
from: new Date(absoluteRange.from).toISOString(),
|
|
|
|
to: new Date(absoluteRange.to).toISOString(),
|
|
|
|
};
|
|
|
|
if (!config.featureToggles.logsInfiniteScrolling) {
|
|
|
|
return range;
|
|
|
|
}
|
|
|
|
|
|
|
|
// With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust
|
|
|
|
// Look for the previous sibling log, and use its timestamp
|
|
|
|
const allLogs = rows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId);
|
|
|
|
const prevLog = getPreviousLog(row, allLogs);
|
|
|
|
|
|
|
|
if (row.timeEpochMs > absoluteRange.to && !prevLog) {
|
|
|
|
// Because there's no sibling and the current `to` is oldest than the log, we have no reference we can use for the interval
|
|
|
|
// This only happens when you scroll into the future and you want to share the first log of the list
|
|
|
|
return {
|
|
|
|
from: new Date(absoluteRange.from).toISOString(),
|
|
|
|
// Slide 1ms otherwise it's very likely to be omitted in the results
|
|
|
|
to: new Date(row.timeEpochMs + 1).toISOString(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
from: new Date(absoluteRange.from).toISOString(),
|
|
|
|
to: new Date(prevLog ? prevLog.timeEpochMs : absoluteRange.to).toISOString(),
|
|
|
|
};
|
|
|
|
}
|