Explore: Create menu for short link button (#77336)

* WIP first pass

* Change to toolbarbutton/buttonselect pattern

* Move to two toolbarbuttons with dropdown

* Justify text to the right of icons

* Fix betterer

* Fix styling and tests

* Add to docs

* Perist last clicked, add translations

* move styling to core component

* use label for tooltip, let parser combine panes in multiple params

* Explore: Panes encoding suggestions (#78210)

panes encoding suggestions

* WIP-add menu groups

* Get group actions working

* add icons and non-local last selected state

* Fix translations after merge

* Add categories to translation, tweak the verbiage

* Fix translation extraction

* Fix tests

* fix translation conflict

* Log if time is absolute

---------

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
Kristina 2023-12-12 09:55:03 -06:00 committed by GitHub
parent 117ecf0403
commit bc8ad7b6e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 244 additions and 25 deletions

View File

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

View File

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

View File

@ -17,6 +17,7 @@ jest.mock('app/core/store', () => {
return {
getBool: jest.fn(),
getObject: jest.fn(),
get: jest.fn(),
};
});

View File

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

View File

@ -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 = [
<DashNavButton
key="share"
tooltip={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
icon="share-alt"
onClick={onCopyShortLink}
aria-label={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
/>,
<div style={{ flex: 1 }} key="spacer0" />,
];
const navBarActions = [<ShortLinkButtonMenu key="share" />, <div style={{ flex: 1 }} key="spacer0" />];
return (
<div>

View File

@ -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 = (
<Menu>
{menuOptions.map((groupOption) => {
return (
<MenuGroup key={groupOption.key} label={groupOption.label}>
{groupOption.items.map((option) => {
return (
<Menu.Item
key={option.key}
label={option.label}
icon={option.icon}
onClick={() => {
const url = option.getUrl();
onCopyLink(option.shorten, option.absTime, url);
setLastSelected(option);
}}
/>
);
})}
</MenuGroup>
);
})}
</Menu>
);
// we need the Toolbar button click to be an action separate from opening/closing the menu
return (
<ToolbarButtonRow>
<Stack gap={0} direction="row" alignItems="center" wrap="nowrap">
<ToolbarButton
tooltip={lastSelected.label}
icon={lastSelected.icon}
iconOnly={true}
narrow={true}
onClick={() => {
const url = lastSelected.getUrl();
onCopyLink(lastSelected.shorten, lastSelected.absTime, url);
}}
aria-label={t('explore.toolbar.copy-shortened-link', 'Copy shortened URL')}
/>
<Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={setIsOpen}>
<ToolbarButton
narrow={true}
isOpen={isOpen}
aria-label={t('explore.toolbar.copy-shortened-link-menu', 'Open copy link options')}
/>
</Dropdown>
</Stack>
</ToolbarButtonRow>
);
}

View File

@ -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<string, ExploreItemState | undefined>) => {
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) });
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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ľőşę",

View File

@ -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": "关闭",