From a0932f4d2a493b2c1e608dbd7c9be125221b7eef Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:31:58 +0200 Subject: [PATCH] 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 --- public/app/core/utils/shortLinks.test.ts | 29 +++++++++++++++ public/app/core/utils/shortLinks.ts | 36 +++++++++++++++++++ .../components/ShareModal/ShareLink.test.tsx | 21 +++++++---- .../components/ShareModal/ShareLink.tsx | 25 ++++++------- .../dashboard/components/ShareModal/utils.ts | 27 +++++--------- .../app/features/explore/ExploreToolbar.tsx | 18 ++-------- .../explore/RichHistory/RichHistoryCard.tsx | 16 ++++----- public/app/features/explore/utils/links.ts | 23 +----------- 8 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 public/app/core/utils/shortLinks.test.ts create mode 100644 public/app/core/utils/shortLinks.ts diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts new file mode 100644 index 00000000000..bbfdd4cf238 --- /dev/null +++ b/public/app/core/utils/shortLinks.test.ts @@ -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'); + }); +}); diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts new file mode 100644 index 00000000000..ab1b275c76a --- /dev/null +++ b/public/app/core/utils/shortLinks.ts @@ -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']); + } +}; diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx index c43b86a3035..9e2f89e7265 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx @@ -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' diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.tsx index e2a1dc504b0..d8d8d66e35d 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.tsx @@ -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 { } } - 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 { checked={includeTemplateVars} onChange={this.onIncludeTemplateVarsChange} /> -