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
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 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
......@@ -60,7 +66,7 @@ Query history lets you view the history of your querying. For each individual qu
- Run a query.
- Create and/or edit a comment.
- 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.
### Manage favorite queries
......
......@@ -481,3 +481,12 @@ export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[])
const refId = getValueWithRefId(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) =>
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) => {
const exploreState: ExploreUrlState = {
/* Default range, as we are not saving timerange in rich history */
......
......@@ -7,9 +7,11 @@ import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore';
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 { StoreState } from 'app/types/store';
import { copyStringToClipboard } from 'app/core/utils/explore';
import appEvents from 'app/core/app_events';
import {
cancelQueries,
changeDatasource,
......@@ -25,6 +27,7 @@ import { getTimeZone } from '../profile/state/selectors';
import { updateTimeZoneForSession } from '../profile/state/reducers';
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
import kbn from '../../core/utils/kbn';
import { createShortLink } from './utils/links';
import { ExploreTimeControls } from './ExploreTimeControls';
import { LiveTailButton } from './LiveTailButton';
import { ResponsiveButton } from './ResponsiveButton';
......@@ -152,6 +155,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
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() {
const {
datasourceMissing,
......@@ -262,6 +275,13 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
/>
</div>
) : 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 && (
<div className="explore-toolbar-content-item">
<ExploreTimeControls
......
......@@ -6,7 +6,9 @@ import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/
import { getDataSourceSrv } from '@grafana/runtime';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
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 { StoreState, CoreEvents } from 'app/types';
......@@ -176,10 +178,15 @@ export function RichHistoryCard(props: Props) {
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
};
const onCreateLink = () => {
const url = createUrlFromRichHistory(query);
copyStringToClipboard(url);
appEvents.emit(AppEvents.alertSuccess, ['Link copied to clipboard']);
const onCreateLink = async () => {
const link = createUrlFromRichHistory(query);
const shortLink = await createShortLink(link);
if (shortLink) {
copyStringToClipboard(shortLink);
appEvents.emit(AppEvents.alertSuccess, ['Shortened link copied to clipboard']);
} else {
appEvents.emit(AppEvents.alertError, ['Error generating shortened link']);
}
};
const onDeleteQuery = () => {
......@@ -254,7 +261,7 @@ export function RichHistoryCard(props: Props) {
title={query.comment?.length > 0 ? 'Edit comment' : 'Add comment'}
/>
<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={query.starred ? 'favorite' : 'star'}
......
import memoizeOne from 'memoize-one';
import { splitOpen } from '../state/actions';
import { Field, LinkModel, TimeRange } from '@grafana/data';
import { getLinkSrv } from '../../panel/panellinks/link_srv';
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
......@@ -60,3 +61,23 @@ function getTitleFromHref(href: string): string {
}
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