mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
117ecf0403
commit
bc8ad7b6e5
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ jest.mock('app/core/store', () => {
|
||||
return {
|
||||
getBool: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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>
|
||||
|
157
public/app/features/explore/ShortLinkButtonMenu.tsx
Normal file
157
public/app/features/explore/ShortLinkButtonMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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) });
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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ľőşę",
|
||||
|
@ -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": "关闭",
|
||||
|
Loading…
Reference in New Issue
Block a user