diff --git a/docs/sources/explore/_index.md b/docs/sources/explore/_index.md index 4ba5d382df6..92257239d95 100644 --- a/docs/sources/explore/_index.md +++ b/docs/sources/explore/_index.md @@ -136,3 +136,11 @@ Available in Grafana 7.3 and later versions. The Share shortened link capability allows you to create smaller and simpler URLs of the format /goto/:uid instead of using longer URLs with query parameters. To create a shortened link to the executed query, click the **Share** option in the Explore toolbar. A shortened link will automatically get deleted after seven (7) days from its creation if it's never used. If a link is used at least once, it won't ever get deleted. + +### Sharing shortened links with absolute time + +{{% admonition type="note" %}} +Available in Grafana 10.3 and later versions. +{{% /admonition %}} + +Short links have two options - keeping relative time (for example, from two hours ago to now) or absolute time (for example, from 8am to 10am). Sharing a shortened link by default will copy the time range selected, relative or absolute. Clicking the dropdown button next to the share shortened link button and selecting one of the options under "Time-Sync URL Links" will allow you to create a short link with the absolute time - meaning anyone receiving the link will see the same data you are seeing, even if they open the link at another time. This will not affect your selected time range. diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index afb606427ba..e304eac1a7d 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -342,10 +342,14 @@ export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: n } export const copyStringToClipboard = (string: string) => { - const el = document.createElement('textarea'); - el.value = string; - document.body.appendChild(el); - el.select(); - document.execCommand('copy'); - document.body.removeChild(el); + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(string); + } else { + const el = document.createElement('textarea'); + el.value = string; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + } }; diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index a886f7dba5c..ca9e3a93a70 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -17,6 +17,7 @@ jest.mock('app/core/store', () => { return { getBool: jest.fn(), getObject: jest.fn(), + get: jest.fn(), }; }); diff --git a/public/app/features/dashboard/utils/dashboard.test.ts b/public/app/features/dashboard/utils/dashboard.test.ts index ec9c3733038..93ca6ece84a 100644 --- a/public/app/features/dashboard/utils/dashboard.test.ts +++ b/public/app/features/dashboard/utils/dashboard.test.ts @@ -10,6 +10,7 @@ jest.mock('app/core/store', () => ({ exists: jest.fn(), getObject: jest.fn(), setObject: jest.fn(), + get: jest.fn(), })); const store = jest.requireMock('app/core/store'); diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 6016ebf538b..0cbb25e78aa 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -16,18 +16,17 @@ import { } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { t, Trans } from 'app/core/internationalization'; -import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore'; import { StoreState, useDispatch, useSelector } from 'app/types/store'; import { contextSrv } from '../../core/core'; -import { DashNavButton } from '../dashboard/components/DashNav/DashNavButton'; import { updateFiscalYearStartMonthForSession, updateTimeZoneForSession } from '../profile/state/reducers'; import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors'; import { ExploreTimeControls } from './ExploreTimeControls'; import { LiveTailButton } from './LiveTailButton'; +import { ShortLinkButtonMenu } from './ShortLinkButtonMenu'; import { ToolbarExtensionPoint } from './extensions/ToolbarExtensionPoint'; import { changeDatasource } from './state/datasource'; import { changeCorrelationHelperData } from './state/explorePane'; @@ -99,11 +98,6 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle ? t('explore.toolbar.refresh-picker-cancel', 'Cancel') : t('explore.toolbar.refresh-picker-run', 'Run query'); - const onCopyShortLink = () => { - createAndCopyShortLink(global.location.href); - reportInteraction('grafana_explore_shortened_link_clicked'); - }; - const onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => { if (!isCorrelationsEditorMode) { dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true })); @@ -206,16 +200,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle dispatch(changeRefreshInterval({ exploreId, refreshInterval })); }; - const navBarActions = [ - , -
, - ]; + const navBarActions = [,
]; return (
diff --git a/public/app/features/explore/ShortLinkButtonMenu.tsx b/public/app/features/explore/ShortLinkButtonMenu.tsx new file mode 100644 index 00000000000..5c7bebcc00f --- /dev/null +++ b/public/app/features/explore/ShortLinkButtonMenu.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; + +import { IconName } from '@grafana/data'; +import { reportInteraction, config } from '@grafana/runtime'; +import { ToolbarButton, Dropdown, Menu, Stack, ToolbarButtonRow, MenuGroup } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { copyStringToClipboard } from 'app/core/utils/explore'; +import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; +import { useSelector } from 'app/types'; + +import { selectPanes } from './state/selectors'; +import { constructAbsoluteUrl } from './utils/links'; + +interface ShortLinkGroupData { + key: string; + label: string; + items: ShortLinkMenuItemData[]; +} + +interface ShortLinkMenuItemData { + key: string; + label: string; + icon: IconName; + getUrl: Function; + shorten: boolean; + absTime: boolean; +} + +const defaultMode: ShortLinkMenuItemData = { + key: 'copy-link', + label: t('explore.toolbar.copy-shortened-link', 'Copy shortened URL'), + icon: 'share-alt', + getUrl: () => undefined, + shorten: true, + absTime: false, +}; + +export function ShortLinkButtonMenu() { + const panes = useSelector(selectPanes); + const [isOpen, setIsOpen] = useState(false); + const [lastSelected, setLastSelected] = useState(defaultMode); + const onCopyLink = (shorten: boolean, absTime: boolean, url?: string) => { + if (shorten) { + createAndCopyShortLink(url || global.location.href); + reportInteraction('grafana_explore_shortened_link_clicked', { isAbsoluteTime: absTime }); + } else { + copyStringToClipboard( + url !== undefined + ? `${window.location.protocol}//${window.location.host}${config.appSubUrl}${url}` + : global.location.href + ); + reportInteraction('grafana_explore_copy_link_clicked', { isAbsoluteTime: absTime }); + } + }; + + const menuOptions: ShortLinkGroupData[] = [ + { + key: 'normal', + label: t('explore.toolbar.copy-links-normal-category', 'Normal URL links'), + items: [ + { + key: 'copy-shortened-link', + icon: 'link', + label: t('explore.toolbar.copy-shortened-link', 'Copy shortened URL'), + getUrl: () => undefined, + shorten: true, + absTime: false, + }, + { + key: 'copy-link', + icon: 'link', + label: t('explore.toolbar.copy-link', 'Copy URL'), + getUrl: () => undefined, + shorten: false, + absTime: false, + }, + ], + }, + { + key: 'timesync', + label: t('explore.toolbar.copy-links-absolute-category', 'Time-sync URL links (share with time range intact)'), + items: [ + { + key: 'copy-short-link-abs-time', + icon: 'clock-nine', + label: t('explore.toolbar.copy-shortened-link-abs-time', 'Copy Absolute Shortened URL'), + shorten: true, + getUrl: () => { + return constructAbsoluteUrl(panes); + }, + absTime: true, + }, + { + key: 'copy-link-abs-time', + icon: 'clock-nine', + label: t('explore.toolbar.copy-link-abs-time', 'Copy Absolute Shortened URL'), + shorten: false, + getUrl: () => { + return constructAbsoluteUrl(panes); + }, + absTime: true, + }, + ], + }, + ]; + + const MenuActions = ( + + {menuOptions.map((groupOption) => { + return ( + + {groupOption.items.map((option) => { + return ( + { + const url = option.getUrl(); + onCopyLink(option.shorten, option.absTime, url); + setLastSelected(option); + }} + /> + ); + })} + + ); + })} + + ); + + // we need the Toolbar button click to be an action separate from opening/closing the menu + return ( + + + { + const url = lastSelected.getUrl(); + onCopyLink(lastSelected.shorten, lastSelected.absTime, url); + }} + aria-label={t('explore.toolbar.copy-shortened-link', 'Copy shortened URL')} + /> + + + + + + ); +} diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index fe686153aeb..0c95005ca91 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -17,13 +17,17 @@ import { CoreApp, SplitOpenOptions, DataLinkPostProcessor, + ExploreUrlState, + urlUtil, } from '@grafana/data'; import { getTemplateSrv, reportInteraction, VariableInterpolation } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { contextSrv } from 'app/core/services/context_srv'; import { getTransformationVars } from 'app/features/correlations/transformations'; +import { ExploreItemState } from 'app/types/explore'; import { getLinkSrv } from '../../panel/panellinks/link_srv'; +import { getUrlStateFromPaneState } from '../hooks/useStateSync'; type DataLinkFilter = (link: DataLink, scopedVars: ScopedVars) => boolean; @@ -328,3 +332,26 @@ function getStringsFromObject(obj: Object): string { } return acc; } + +type StateEntry = [string, ExploreItemState]; +const isStateEntry = (entry: [string, ExploreItemState | undefined]): entry is StateEntry => { + return entry[1] !== undefined; +}; + +export const constructAbsoluteUrl = (panes: Record) => { + const urlStates = Object.entries(panes) + .filter(isStateEntry) + .map(([exploreId, pane]) => { + const urlState = getUrlStateFromPaneState(pane); + urlState.range = { + to: pane.range.to.valueOf().toString(), + from: pane.range.from.valueOf().toString(), + }; + const panes: [string, ExploreUrlState] = [exploreId, urlState]; + return panes; + }) + .reduce((acc, [exploreId, urlState]) => { + return { ...acc, [exploreId]: urlState }; + }, {}); + return urlUtil.renderUrl('/explore', { schemaVersion: 1, panes: JSON.stringify(urlStates) }); +}; diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index db42be07e05..12c63683cc5 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -527,7 +527,13 @@ }, "toolbar": { "aria-label": "Werkzeugleiste durchsuchen", + "copy-link": "", + "copy-link-abs-time": "", + "copy-links-absolute-category": "", + "copy-links-normal-category": "", "copy-shortened-link": "Verkürzten Link kopieren", + "copy-shortened-link-abs-time": "", + "copy-shortened-link-menu": "", "refresh-picker-cancel": "Abbrechen", "refresh-picker-run": "Abfrage ausführen", "split-close": "Schließen", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index dfa8029486c..8b9203d23db 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -527,7 +527,13 @@ }, "toolbar": { "aria-label": "Explore toolbar", - "copy-shortened-link": "Copy shortened link", + "copy-link": "Copy URL", + "copy-link-abs-time": "Copy absolute URL", + "copy-links-absolute-category": "Time-sync URL links (share with time range intact)", + "copy-links-normal-category": "Normal URL links", + "copy-shortened-link": "Copy shortened URL", + "copy-shortened-link-abs-time": "Copy absolute shortened URL", + "copy-shortened-link-menu": "Open shortened link menu", "refresh-picker-cancel": "Cancel", "refresh-picker-run": "Run query", "split-close": "Close", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index aaff27da61b..24e9f4fe743 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -532,7 +532,13 @@ }, "toolbar": { "aria-label": "Barra de herramientas de Explore", + "copy-link": "", + "copy-link-abs-time": "", + "copy-links-absolute-category": "", + "copy-links-normal-category": "", "copy-shortened-link": "Copiar enlace acortado", + "copy-shortened-link-abs-time": "", + "copy-shortened-link-menu": "", "refresh-picker-cancel": "Cancelar", "refresh-picker-run": "Ejecutar consulta", "split-close": "Cerrar", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index e0baa3ec84d..6e2789b7d7a 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -532,7 +532,13 @@ }, "toolbar": { "aria-label": "Explorer la barre d'outils", + "copy-link": "", + "copy-link-abs-time": "", + "copy-links-absolute-category": "", + "copy-links-normal-category": "", "copy-shortened-link": "Copier le lien raccourci", + "copy-shortened-link-abs-time": "", + "copy-shortened-link-menu": "", "refresh-picker-cancel": "Annuler", "refresh-picker-run": "Exécuter la requête", "split-close": "Fermer", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 6118464aa77..14d7b18e01f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -527,7 +527,13 @@ }, "toolbar": { "aria-label": "Ēχpľőřę ŧőőľþäř", - "copy-shortened-link": "Cőpy şĥőřŧęʼnęđ ľįʼnĸ", + "copy-link": "Cőpy ŮŖĿ", + "copy-link-abs-time": "Cőpy äþşőľūŧę ŮŖĿ", + "copy-links-absolute-category": "Ŧįmę-şyʼnč ŮŖĿ ľįʼnĸş (şĥäřę ŵįŧĥ ŧįmę řäʼnģę įʼnŧäčŧ)", + "copy-links-normal-category": "Ńőřmäľ ŮŖĿ ľįʼnĸş", + "copy-shortened-link": "Cőpy şĥőřŧęʼnęđ ŮŖĿ", + "copy-shortened-link-abs-time": "Cőpy äþşőľūŧę şĥőřŧęʼnęđ ŮŖĿ", + "copy-shortened-link-menu": "Øpęʼn şĥőřŧęʼnęđ ľįʼnĸ męʼnū", "refresh-picker-cancel": "Cäʼnčęľ", "refresh-picker-run": "Ŗūʼn qūęřy", "split-close": "Cľőşę", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 79497d045dd..2de1e243909 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -522,7 +522,13 @@ }, "toolbar": { "aria-label": "浏览 工具栏", + "copy-link": "", + "copy-link-abs-time": "", + "copy-links-absolute-category": "", + "copy-links-normal-category": "", "copy-shortened-link": "复制短链接", + "copy-shortened-link-abs-time": "", + "copy-shortened-link-menu": "", "refresh-picker-cancel": "取消", "refresh-picker-run": "运行查询", "split-close": "关闭",