Commit db071e49 by Ivana Huckova Committed by GitHub

Explore: Allow shortlink generation (#28222)

* Add short link functionality to Explore and Rich history

* Update documentation

* Implement short url for explore

* Implement short link in Rich history

* Update docs

* Add error alert
parent fe15d90e
...@@ -49,9 +49,15 @@ In split view, timepickers for both panels can be linked (if you change one, the ...@@ -49,9 +49,15 @@ In split view, timepickers for both panels can be linked (if you change one, the
You can close the newly created query by clicking on the Close Split button. You can close the newly created query by clicking on the Close Split button.
## Share shortened link
> Share shortened link is only available in Grafana 7.3 and above.
The Share shortened link capability allows you to create smaller and simpler URLs of the format /goto/:uid instead of using longer URLs containing complex query parameters. You can create a shortened link by clicking on the **Share** option in Explore toolbar.
## Query history ## Query history
Query history is a list of queries that you have used in Explore. The history is local to your browser and is not shared with others. To open and interact with your history, click the **Query history** button in Explore. Query history is a list of queries that you have used in Explore. The history is local to your browser and is not shared. To open and interact with your history, click the **Query history** button in Explore.
### View query history ### View query history
...@@ -60,7 +66,7 @@ Query history lets you view the history of your querying. For each individual qu ...@@ -60,7 +66,7 @@ Query history lets you view the history of your querying. For each individual qu
- Run a query. - Run a query.
- Create and/or edit a comment. - Create and/or edit a comment.
- Copy a query to the clipboard. - Copy a query to the clipboard.
- Copy a URL link with the query to the clipboard. - Copy a shortened link with the query to the clipboard.
- Star a query. - Star a query.
### Manage favorite queries ### Manage favorite queries
......
...@@ -481,3 +481,12 @@ export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]) ...@@ -481,3 +481,12 @@ export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[])
const refId = getValueWithRefId(queryErrors); const refId = getValueWithRefId(queryErrors);
return refId ? undefined : getFirstQueryErrorWithoutRefId(queryErrors); return refId ? undefined : getFirstQueryErrorWithoutRefId(queryErrors);
}; };
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);
};
...@@ -170,15 +170,6 @@ export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) => ...@@ -170,15 +170,6 @@ export const sortQueries = (array: RichHistoryQuery[], sortOrder: SortOrder) =>
return array.sort(sortFunc); return array.sort(sortFunc);
}; };
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);
};
export const createUrlFromRichHistory = (query: RichHistoryQuery) => { export const createUrlFromRichHistory = (query: RichHistoryQuery) => {
const exploreState: ExploreUrlState = { const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */ /* Default range, as we are not saving timerange in rich history */
......
...@@ -7,9 +7,11 @@ import { css } from 'emotion'; ...@@ -7,9 +7,11 @@ import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui'; import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
import { DataQuery, RawTimeRange, TimeRange, TimeZone } from '@grafana/data'; import { DataQuery, RawTimeRange, TimeRange, TimeZone, AppEvents } from '@grafana/data';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store'; import { StoreState } from 'app/types/store';
import { copyStringToClipboard } from 'app/core/utils/explore';
import appEvents from 'app/core/app_events';
import { import {
cancelQueries, cancelQueries,
changeDatasource, changeDatasource,
...@@ -25,6 +27,7 @@ import { getTimeZone } from '../profile/state/selectors'; ...@@ -25,6 +27,7 @@ import { getTimeZone } from '../profile/state/selectors';
import { updateTimeZoneForSession } from '../profile/state/reducers'; import { updateTimeZoneForSession } from '../profile/state/reducers';
import { getDashboardSrv } from '../dashboard/services/DashboardSrv'; import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
import kbn from '../../core/utils/kbn'; import kbn from '../../core/utils/kbn';
import { createShortLink } from './utils/links';
import { ExploreTimeControls } from './ExploreTimeControls'; import { ExploreTimeControls } from './ExploreTimeControls';
import { LiveTailButton } from './LiveTailButton'; import { LiveTailButton } from './LiveTailButton';
import { ResponsiveButton } from './ResponsiveButton'; import { ResponsiveButton } from './ResponsiveButton';
...@@ -152,6 +155,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -152,6 +155,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
return datasourceName ? exploreDatasources.find(datasource => datasource.name === datasourceName) : undefined; return datasourceName ? exploreDatasources.find(datasource => datasource.name === datasourceName) : undefined;
}; };
copyAndSaveShortLink = async () => {
const shortLink = await createShortLink(window.location.href);
if (shortLink) {
copyStringToClipboard(shortLink);
appEvents.emit(AppEvents.alertSuccess, ['Shortened link copied to clipboard']);
} else {
appEvents.emit(AppEvents.alertError, ['Error generating shortened link']);
}
};
render() { render() {
const { const {
datasourceMissing, datasourceMissing,
...@@ -262,6 +275,13 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -262,6 +275,13 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
/> />
</div> </div>
) : null} ) : null}
<div className={'explore-toolbar-content-item'}>
<Tooltip content={'Copy shortened link'} placement="bottom">
<button className={'btn navbar-button'} onClick={this.copyAndSaveShortLink}>
<Icon name="share-alt" />
</button>
</Tooltip>
</div>
{!isLive && ( {!isLive && (
<div className="explore-toolbar-content-item"> <div className="explore-toolbar-content-item">
<ExploreTimeControls <ExploreTimeControls
......
...@@ -6,7 +6,9 @@ import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ ...@@ -6,7 +6,9 @@ import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data'; import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
import { RichHistoryQuery, ExploreId } from 'app/types/explore'; import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { copyStringToClipboard, createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory'; import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
import { createShortLink } from '../../explore/utils/links';
import { copyStringToClipboard } from 'app/core/utils/explore';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { StoreState, CoreEvents } from 'app/types'; import { StoreState, CoreEvents } from 'app/types';
...@@ -176,10 +178,15 @@ export function RichHistoryCard(props: Props) { ...@@ -176,10 +178,15 @@ export function RichHistoryCard(props: Props) {
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']); appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
}; };
const onCreateLink = () => { const onCreateLink = async () => {
const url = createUrlFromRichHistory(query); const link = createUrlFromRichHistory(query);
copyStringToClipboard(url); const shortLink = await createShortLink(link);
appEvents.emit(AppEvents.alertSuccess, ['Link copied to clipboard']); if (shortLink) {
copyStringToClipboard(shortLink);
appEvents.emit(AppEvents.alertSuccess, ['Shortened link copied to clipboard']);
} else {
appEvents.emit(AppEvents.alertError, ['Error generating shortened link']);
}
}; };
const onDeleteQuery = () => { const onDeleteQuery = () => {
...@@ -254,7 +261,7 @@ export function RichHistoryCard(props: Props) { ...@@ -254,7 +261,7 @@ export function RichHistoryCard(props: Props) {
title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'} title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
/> />
<IconButton name="copy" onClick={onCopyQuery} title="Copy query to clipboard" /> <IconButton name="copy" onClick={onCopyQuery} title="Copy query to clipboard" />
{!isRemoved && <IconButton name="link" onClick={onCreateLink} title="Copy link to clipboard" />} {!isRemoved && <IconButton name="share-alt" onClick={onCreateLink} title="Copy shortened link to clipboard" />}
<IconButton name="trash-alt" title={'Delete query'} onClick={onDeleteQuery} /> <IconButton name="trash-alt" title={'Delete query'} onClick={onDeleteQuery} />
<IconButton <IconButton
name={query.starred ? 'favorite' : 'star'} name={query.starred ? 'favorite' : 'star'}
......
import memoizeOne from 'memoize-one';
import { splitOpen } from '../state/actions'; import { splitOpen } from '../state/actions';
import { Field, LinkModel, TimeRange } from '@grafana/data'; import { Field, LinkModel, TimeRange } from '@grafana/data';
import { getLinkSrv } from '../../panel/panellinks/link_srv'; import { getLinkSrv } from '../../panel/panellinks/link_srv';
import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks'; import { mapInternalLinkToExplore } from '@grafana/data/src/utils/dataLinks';
import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; import { getDataSourceSrv, getTemplateSrv, getBackendSrv, config } from '@grafana/runtime';
/** /**
* Get links from the field of a dataframe and in addition check if there is associated * Get links from the field of a dataframe and in addition check if there is associated
...@@ -60,3 +61,23 @@ function getTitleFromHref(href: string): string { ...@@ -60,3 +61,23 @@ function getTitleFromHref(href: string): string {
} }
return title; return title;
} }
function buildHostUrl() {
return `${window.location.protocol}//${window.location.host}${config.appSubUrl}`;
}
function getRelativeURLPath(url: string) {
let path = url.replace(buildHostUrl(), '');
return path.startsWith('/') ? path.substring(1, path.length) : path;
}
export const createShortLink = memoizeOne(async function(path: string) {
try {
const shortUrl = await getBackendSrv().post(`/api/short-urls`, {
path: getRelativeURLPath(path),
});
return shortUrl.url;
} catch (err) {
console.error('Error when creating shortened link: ', err);
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment