mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Shorten url: Unification across Explore and Dashboards (#28434)
* WIP: Unify short url for dashboards and explore * Add tests * Update * Address feedback, move createShortUrl to buildUrl
This commit is contained in:
parent
8f4be08b00
commit
a0932f4d2a
29
public/app/core/utils/shortLinks.test.ts
Normal file
29
public/app/core/utils/shortLinks.test.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { createShortLink, createAndCopyShortLink } from './shortLinks';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
post: () => {
|
||||
return Promise.resolve({ url: 'www.short.com' });
|
||||
},
|
||||
};
|
||||
},
|
||||
config: {
|
||||
appSubUrl: '',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('createShortLink', () => {
|
||||
it('creates short link', async () => {
|
||||
const shortUrl = await createShortLink('www.verylonglinkwehavehere.com');
|
||||
expect(shortUrl).toBe('www.short.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAndCopyShortLink', () => {
|
||||
it('copies short link to clipboard', async () => {
|
||||
document.execCommand = jest.fn();
|
||||
await createAndCopyShortLink('www.verylonglinkwehavehere.com');
|
||||
expect(document.execCommand).toHaveBeenCalledWith('copy');
|
||||
});
|
||||
});
|
36
public/app/core/utils/shortLinks.ts
Normal file
36
public/app/core/utils/shortLinks.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { getBackendSrv, config } from '@grafana/runtime';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { copyStringToClipboard } from './explore';
|
||||
|
||||
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 shortLink = await getBackendSrv().post(`/api/short-urls`, {
|
||||
path: getRelativeURLPath(path),
|
||||
});
|
||||
return shortLink.url;
|
||||
} catch (err) {
|
||||
console.error('Error when creating shortened link: ', err);
|
||||
appEvents.emit(AppEvents.alertError, ['Error generating shortened link']);
|
||||
}
|
||||
});
|
||||
|
||||
export const createAndCopyShortLink = async (path: string) => {
|
||||
const shortLink = await createShortLink(path);
|
||||
if (shortLink) {
|
||||
copyStringToClipboard(shortLink);
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Shortened link copied to clipboard']);
|
||||
} else {
|
||||
appEvents.emit(AppEvents.alertError, ['Error generating shortened link']);
|
||||
}
|
||||
};
|
@ -111,64 +111,70 @@ describe('ShareModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate share url absolute time', () => {
|
||||
it('should generate share url absolute time', async () => {
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&viewPanel=22');
|
||||
});
|
||||
|
||||
it('should generate render url', () => {
|
||||
it('should generate render url', async () => {
|
||||
mockLocationHref('http://dashboards.grafana.com/d/abcdefghi/my-dash');
|
||||
ctx.mount({
|
||||
panel: { id: 22, options: {}, fieldConfig: { defaults: {}, overrides: [] } },
|
||||
});
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
const base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
|
||||
const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(state?.imageUrl).toContain(base + params);
|
||||
});
|
||||
|
||||
it('should generate render url for scripted dashboard', () => {
|
||||
it('should generate render url for scripted dashboard', async () => {
|
||||
mockLocationHref('http://dashboards.grafana.com/dashboard/script/my-dash.js');
|
||||
ctx.mount({
|
||||
panel: { id: 22, options: {}, fieldConfig: { defaults: {}, overrides: [] } },
|
||||
});
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
const base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
|
||||
const params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(state?.imageUrl).toContain(base + params);
|
||||
});
|
||||
|
||||
it('should remove panel id when no panel in scope', () => {
|
||||
it('should remove panel id when no panel in scope', async () => {
|
||||
ctx.mount({
|
||||
panel: undefined,
|
||||
});
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1');
|
||||
});
|
||||
|
||||
it('should add theme when specified', () => {
|
||||
it('should add theme when specified', async () => {
|
||||
ctx.wrapper?.setProps({ panel: undefined });
|
||||
ctx.wrapper?.setState({ selectedTheme: { label: 'light', value: 'light' } });
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
expect(state?.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
|
||||
});
|
||||
|
||||
it('should remove editPanel from image url when is first param in querystring', () => {
|
||||
it('should remove editPanel from image url when is first param in querystring', async () => {
|
||||
mockLocationHref('http://server/#!/test?editPanel=1');
|
||||
ctx.mount({
|
||||
panel: { id: 1, options: {}, fieldConfig: { defaults: {}, overrides: [] } },
|
||||
});
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
expect(state?.shareUrl).toContain('?editPanel=1&from=1000&to=2000&orgId=1');
|
||||
expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
|
||||
});
|
||||
|
||||
it('should include template variables in url', () => {
|
||||
it('should include template variables in url', async () => {
|
||||
mockLocationHref('http://server/#!/test');
|
||||
fillVariableValuesForUrlMock = (params: any) => {
|
||||
params['var-app'] = 'mupp';
|
||||
@ -177,6 +183,7 @@ describe('ShareModal', () => {
|
||||
ctx.mount();
|
||||
ctx.wrapper?.setState({ includeTemplateVars: true });
|
||||
|
||||
await ctx.wrapper?.instance().buildUrl();
|
||||
const state = ctx.wrapper?.state();
|
||||
expect(state?.shareUrl).toContain(
|
||||
'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01'
|
||||
|
@ -3,9 +3,8 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { LegacyForms, ClipboardButton, Icon, InfoBox, Input } from '@grafana/ui';
|
||||
const { Select, Switch } = LegacyForms;
|
||||
import { SelectableValue, PanelModel, AppEvents } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { buildImageUrl, buildShareUrl, getRelativeURLPath } from './utils';
|
||||
import { buildImageUrl, buildShareUrl } from './utils';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import config from 'app/core/config';
|
||||
|
||||
@ -58,22 +57,20 @@ export class ShareLink extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
buildUrl = () => {
|
||||
buildUrl = async () => {
|
||||
const { panel } = this.props;
|
||||
const { useCurrentTimeRange, includeTemplateVars, useShortUrl, selectedTheme } = this.state;
|
||||
|
||||
const shareUrl = buildShareUrl(useCurrentTimeRange, includeTemplateVars, selectedTheme.value, panel);
|
||||
const shareUrl = await buildShareUrl(
|
||||
useCurrentTimeRange,
|
||||
includeTemplateVars,
|
||||
selectedTheme.value,
|
||||
panel,
|
||||
useShortUrl
|
||||
);
|
||||
const imageUrl = buildImageUrl(useCurrentTimeRange, includeTemplateVars, selectedTheme.value, panel);
|
||||
|
||||
if (useShortUrl) {
|
||||
getBackendSrv()
|
||||
.post(`/api/short-urls`, {
|
||||
path: getRelativeURLPath(shareUrl),
|
||||
})
|
||||
.then(res => this.setState({ shareUrl: res.url, imageUrl }));
|
||||
} else {
|
||||
this.setState({ shareUrl, imageUrl });
|
||||
}
|
||||
this.setState({ shareUrl, imageUrl });
|
||||
};
|
||||
|
||||
onUseCurrentTimeRangeChange = () => {
|
||||
@ -126,11 +123,11 @@ export class ShareLink extends PureComponent<Props, State> {
|
||||
checked={includeTemplateVars}
|
||||
onChange={this.onIncludeTemplateVarsChange}
|
||||
/>
|
||||
<Switch labelClass="width-12" label="Shorten URL" checked={useShortUrl} onChange={this.onUrlShorten} />
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label width-12">Theme</label>
|
||||
<Select width={10} options={themeOptions} value={selectedTheme} onChange={this.onThemeChange} />
|
||||
</div>
|
||||
<Switch labelClass="width-12" label="Shorten URL" checked={useShortUrl} onChange={this.onUrlShorten} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="gf-form-group">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getTemplateSrv } from 'app/features/templating/template_srv';
|
||||
import { createShortLink } from 'app/core/utils/shortLinks';
|
||||
import { PanelModel, dateTime, urlUtil } from '@grafana/data';
|
||||
|
||||
export function buildParams(
|
||||
@ -38,15 +39,6 @@ export function buildParams(
|
||||
return params;
|
||||
}
|
||||
|
||||
export function buildHostUrl() {
|
||||
return `${window.location.protocol}//${window.location.host}${config.appSubUrl}`;
|
||||
}
|
||||
|
||||
export function getRelativeURLPath(url: string) {
|
||||
let p = url.replace(buildHostUrl(), '');
|
||||
return p.startsWith('/') ? p.substring(1, p.length) : p;
|
||||
}
|
||||
|
||||
export function buildBaseUrl() {
|
||||
let baseUrl = window.location.href;
|
||||
const queryStart = baseUrl.indexOf('?');
|
||||
@ -58,16 +50,20 @@ export function buildBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
export function buildShareUrl(
|
||||
export async function buildShareUrl(
|
||||
useCurrentTimeRange: boolean,
|
||||
includeTemplateVars: boolean,
|
||||
selectedTheme?: string,
|
||||
panel?: PanelModel
|
||||
panel?: PanelModel,
|
||||
shortenUrl?: boolean
|
||||
) {
|
||||
const baseUrl = buildBaseUrl();
|
||||
const params = buildParams(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel);
|
||||
|
||||
return urlUtil.appendQueryToUrl(baseUrl, urlUtil.toUrlParams(params));
|
||||
const shareUrl = urlUtil.appendQueryToUrl(baseUrl, urlUtil.toUrlParams(params));
|
||||
if (shortenUrl) {
|
||||
return await createShortLink(shareUrl);
|
||||
}
|
||||
return shareUrl;
|
||||
}
|
||||
|
||||
export function buildSoloUrl(
|
||||
@ -113,11 +109,6 @@ export function buildIframeHtml(
|
||||
return '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
|
||||
}
|
||||
|
||||
export function buildShortUrl(uid: string) {
|
||||
const hostUrl = buildHostUrl();
|
||||
return `${hostUrl}/goto/${uid}`;
|
||||
}
|
||||
|
||||
export function getLocalTimeZone() {
|
||||
const utcOffset = '&tz=UTC' + encodeURIComponent(dateTime().format('Z'));
|
||||
|
||||
|
@ -7,11 +7,10 @@ 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, AppEvents } from '@grafana/data';
|
||||
import { DataQuery, RawTimeRange, TimeRange, TimeZone } 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 { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import {
|
||||
cancelQueries,
|
||||
changeDatasource,
|
||||
@ -27,7 +26,6 @@ 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';
|
||||
@ -155,16 +153,6 @@ 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,
|
||||
@ -277,7 +265,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
) : null}
|
||||
<div className={'explore-toolbar-content-item'}>
|
||||
<Tooltip content={'Copy shortened link'} placement="bottom">
|
||||
<button className={'btn navbar-button'} onClick={this.copyAndSaveShortLink}>
|
||||
<button className={'btn navbar-button'} onClick={() => createAndCopyShortLink(window.location.href)}>
|
||||
<Icon name="share-alt" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
@ -7,7 +7,7 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
|
||||
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
|
||||
import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
|
||||
import { createShortLink } from '../../explore/utils/links';
|
||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||
import { copyStringToClipboard } from 'app/core/utils/explore';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { StoreState, CoreEvents } from 'app/types';
|
||||
@ -178,15 +178,9 @@ export function RichHistoryCard(props: Props) {
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Query copied to clipboard']);
|
||||
};
|
||||
|
||||
const onCreateLink = async () => {
|
||||
const onCreateShortLink = 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']);
|
||||
}
|
||||
await createAndCopyShortLink(link);
|
||||
};
|
||||
|
||||
const onDeleteQuery = () => {
|
||||
@ -261,7 +255,9 @@ 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="share-alt" onClick={onCreateLink} title="Copy shortened link to clipboard" />}
|
||||
{!isRemoved && (
|
||||
<IconButton name="share-alt" onClick={onCreateShortLink} title="Copy shortened link to clipboard" />
|
||||
)}
|
||||
<IconButton name="trash-alt" title={'Delete query'} onClick={onDeleteQuery} />
|
||||
<IconButton
|
||||
name={query.starred ? 'favorite' : 'star'}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { splitOpen } from '../state/actions';
|
||||
import { Field, LinkModel, TimeRange, mapInternalLinkToExplore } from '@grafana/data';
|
||||
import { getLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { getDataSourceSrv, getTemplateSrv, getBackendSrv, config } from '@grafana/runtime';
|
||||
import { getDataSourceSrv, getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
/**
|
||||
* Get links from the field of a dataframe and in addition check if there is associated
|
||||
@ -60,23 +59,3 @@ 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);
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user