Commit b40b134e by Oscar Kilhed Committed by GitHub

Dashboard: Remove template variables option from ShareModal (#30395)

* Dashboard: Remove template variables option and update style of ShareModal (#29191)

* Simplified design

* Changed to text area component for embed and replaced select with RadioButtonGroup

* Use primitive string instead of SelectableValue in the states

* Changed embed html TextArea to writable and added a copy to clipboard button

* Added some space between the buttons on the snapshot tab and removed unnessecary FieldSet elements

* Add descriptions to the tabs that were missing descriptions

* Capitalization of theme names

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 3cf47b24
import React from 'react';
import { cx } from 'emotion';
import { IconName } from '../../types';
import { Icon } from '../Icon/Icon';
interface Props {
/** @deprecated */
icon?: IconName;
/** @deprecated */
iconClass?: string;
}
export const ModalTabContent: React.FC<Props> = ({ icon, iconClass, children }) => {
/** @internal */
export const ModalTabContent: React.FC<Props> = ({ children }) => {
return (
<div className="share-modal-body">
<div className="share-modal-header">
{icon && <Icon name={icon} size="xxl" className={cx(iconClass, 'share-modal-big-icon')} />}
<div className="share-modal-content">{children}</div>
</div>
</div>
......
import React, { PureComponent } from 'react';
import { Select, Switch, Icon, InlineField } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import React, { FormEvent, PureComponent } from 'react';
import { RadioButtonGroup, Switch, Field, TextArea, Icon, ClipboardButton } from '@grafana/ui';
import { SelectableValue, AppEvents } from '@grafana/data';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { appEvents } from 'app/core/core';
import { buildIframeHtml } from './utils';
const themeOptions: Array<SelectableValue<string>> = [
{ label: 'current', value: 'current' },
{ label: 'dark', value: 'dark' },
{ label: 'light', value: 'light' },
{ label: 'Current', value: 'current' },
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
];
interface Props {
......@@ -17,8 +18,7 @@ interface Props {
interface State {
useCurrentTimeRange: boolean;
includeTemplateVars: boolean;
selectedTheme: SelectableValue<string>;
selectedTheme: string;
iframeHtml: string;
}
......@@ -27,8 +27,7 @@ export class ShareEmbed extends PureComponent<Props, State> {
super(props);
this.state = {
useCurrentTimeRange: true,
includeTemplateVars: true,
selectedTheme: themeOptions[0],
selectedTheme: 'current',
iframeHtml: '',
};
}
......@@ -39,12 +38,16 @@ export class ShareEmbed extends PureComponent<Props, State> {
buildIframeHtml = () => {
const { panel } = this.props;
const { useCurrentTimeRange, includeTemplateVars, selectedTheme } = this.state;
const { useCurrentTimeRange, selectedTheme } = this.state;
const iframeHtml = buildIframeHtml(useCurrentTimeRange, includeTemplateVars, selectedTheme.value, panel);
const iframeHtml = buildIframeHtml(useCurrentTimeRange, selectedTheme, panel);
this.setState({ iframeHtml });
};
onIframeHtmlChange = (event: FormEvent<HTMLTextAreaElement>) => {
this.setState({ iframeHtml: event.currentTarget.value });
};
onUseCurrentTimeRangeChange = () => {
this.setState(
{
......@@ -54,61 +57,50 @@ export class ShareEmbed extends PureComponent<Props, State> {
);
};
onIncludeTemplateVarsChange = () => {
this.setState(
{
includeTemplateVars: !this.state.includeTemplateVars,
},
this.buildIframeHtml
);
onThemeChange = (value: string) => {
this.setState({ selectedTheme: value }, this.buildIframeHtml);
};
onThemeChange = (value: SelectableValue<string>) => {
this.setState(
{
selectedTheme: value,
},
this.buildIframeHtml
);
onIframeHtmlCopy = () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
};
getIframeHtml = () => {
return this.state.iframeHtml;
};
render() {
const { useCurrentTimeRange, includeTemplateVars, selectedTheme, iframeHtml } = this.state;
const { useCurrentTimeRange, selectedTheme, iframeHtml } = this.state;
const isRelativeTime = this.props.dashboard ? this.props.dashboard.time.to === 'now' : false;
return (
<div className="share-modal-body">
<div className="share-modal-header">
<Icon name="link" className="share-modal-big-icon" size="xxl" />
<div className="share-modal-content">
<div className="gf-form-group">
<InlineField labelWidth={24} label="Current time range">
<Switch
id="share-current-time-range"
value={useCurrentTimeRange}
onChange={this.onUseCurrentTimeRangeChange}
/>
</InlineField>
<InlineField labelWidth={24} label="Template variables">
<Switch
id="share-template-variables"
value={includeTemplateVars}
onChange={this.onIncludeTemplateVarsChange}
/>
</InlineField>
<InlineField labelWidth={24} label="Theme">
<Select width={20} options={themeOptions} value={selectedTheme} onChange={this.onThemeChange} />
</InlineField>
</div>
<p className="share-modal-info-text">
The html code below can be pasted and included in another web page. Unless anonymous access is enabled,
the user viewing that page need to be signed into grafana for the graph to load.
</p>
<div className="gf-form-group gf-form--grow">
<div className="gf-form">
<textarea rows={5} data-share-panel-url className="gf-form-input" defaultValue={iframeHtml}></textarea>
</div>
</div>
<p className="share-modal-info-text">Generate HTML for embedding an iframe with this panel.</p>
<Field
label="Current time range"
description={isRelativeTime ? 'Transforms the current relative time range to an absolute time range' : ''}
>
<Switch
id="share-current-time-range"
value={useCurrentTimeRange}
onChange={this.onUseCurrentTimeRangeChange}
/>
</Field>
<Field label="Theme">
<RadioButtonGroup options={themeOptions} value={selectedTheme} onChange={this.onThemeChange} />
</Field>
<Field
label="Embed html"
description="The html code below can be pasted and included in another web page. Unless anonymous access is enabled,
the user viewing that page need to be signed into grafana for the graph to load."
>
<TextArea rows={5} value={iframeHtml} onChange={this.onIframeHtmlChange}></TextArea>
</Field>
<ClipboardButton variant="primary" getText={this.getIframeHtml} onClipboardCopy={this.onIframeHtmlCopy}>
<Icon name="copy" /> Copy
</ClipboardButton>
</div>
</div>
</div>
......
import React, { PureComponent } from 'react';
import { saveAs } from 'file-saver';
import { Button, InlineField, Switch, Icon } from '@grafana/ui';
import { Button, Field, Switch } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
import { appEvents } from 'app/core/core';
......@@ -90,11 +90,11 @@ export class ShareExport extends PureComponent<Props, State> {
return (
<div className="share-modal-body">
<div className="share-modal-header">
<Icon name="cloud-upload" size="xxl" className="share-modal-big-icon" />
<div className="share-modal-content">
<InlineField labelWidth={32} label="Export for sharing externally">
<p className="share-modal-info-text">Export this dashboard.</p>
<Field label="Export for sharing externally">
<Switch value={shareExternally} onChange={this.onShareExternallyChange} />
</InlineField>
</Field>
<div className="gf-form-button-row">
<Button variant="primary" onClick={this.onSaveAsFile}>
Save to file
......
......@@ -156,7 +156,7 @@ describe('ShareModal', () => {
it('should add theme when specified', async () => {
ctx.wrapper?.setProps({ panel: undefined });
ctx.wrapper?.setState({ selectedTheme: { label: 'light', value: 'light' } });
ctx.wrapper?.setState({ selectedTheme: 'light' });
await ctx.wrapper?.instance().buildUrl();
const state = ctx.wrapper?.state();
......@@ -175,35 +175,13 @@ describe('ShareModal', () => {
expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
});
describe('template variables', () => {
beforeEach(() => {
templateSrv = initTemplateSrv([
{ type: 'query', name: 'app', current: { value: 'mupp' } },
{ type: 'query', name: 'server', current: { value: 'srv-01' } },
]);
setTemplateSrv(templateSrv);
});
it('should include template variables in url', async () => {
mockLocationHref('http://server/#!/test');
ctx.mount();
ctx.wrapper?.setState({ includeTemplateVars: true });
it('should shorten url', () => {
mockLocationHref('http://server/#!/test');
ctx.mount();
ctx.wrapper?.setState({ useShortUrl: true }, async () => {
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'
);
});
it('should shorten url', () => {
mockLocationHref('http://server/#!/test');
ctx.mount();
ctx.wrapper?.setState({ includeTemplateVars: true, useShortUrl: true }, async () => {
await ctx.wrapper?.instance().buildUrl();
const state = ctx.wrapper?.state();
expect(state?.shareUrl).toContain(`/goto/${mockUid}`);
});
expect(state?.shareUrl).toContain(`/goto/${mockUid}`);
});
});
});
......
import React, { PureComponent } from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { InlineField, Select, Switch, ClipboardButton, Icon, InfoBox, Input } from '@grafana/ui';
import { Field, RadioButtonGroup, Switch, ClipboardButton, Icon, InfoBox, Input, FieldSet } from '@grafana/ui';
import { SelectableValue, PanelModel, AppEvents } from '@grafana/data';
import { DashboardModel } from 'app/features/dashboard/state';
import { buildImageUrl, buildShareUrl } from './utils';
......@@ -8,9 +8,9 @@ import { appEvents } from 'app/core/core';
import config from 'app/core/config';
const themeOptions: Array<SelectableValue<string>> = [
{ label: 'current', value: 'current' },
{ label: 'dark', value: 'dark' },
{ label: 'light', value: 'light' },
{ label: 'Current', value: 'current' },
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },
];
export interface Props {
......@@ -20,9 +20,8 @@ export interface Props {
export interface State {
useCurrentTimeRange: boolean;
includeTemplateVars: boolean;
useShortUrl: boolean;
selectedTheme: SelectableValue<string>;
selectedTheme: string;
shareUrl: string;
imageUrl: string;
}
......@@ -32,9 +31,8 @@ export class ShareLink extends PureComponent<Props, State> {
super(props);
this.state = {
useCurrentTimeRange: true,
includeTemplateVars: true,
useShortUrl: false,
selectedTheme: themeOptions[0],
selectedTheme: 'current',
shareUrl: '',
imageUrl: '',
};
......@@ -45,11 +43,10 @@ export class ShareLink extends PureComponent<Props, State> {
}
componentDidUpdate(prevProps: Props, prevState: State) {
const { useCurrentTimeRange, includeTemplateVars, useShortUrl, selectedTheme } = this.state;
const { useCurrentTimeRange, useShortUrl, selectedTheme } = this.state;
if (
prevState.useCurrentTimeRange !== useCurrentTimeRange ||
prevState.includeTemplateVars !== includeTemplateVars ||
prevState.selectedTheme.value !== selectedTheme.value ||
prevState.selectedTheme !== selectedTheme ||
prevState.useShortUrl !== useShortUrl
) {
this.buildUrl();
......@@ -58,16 +55,10 @@ export class ShareLink extends PureComponent<Props, State> {
buildUrl = async () => {
const { panel } = this.props;
const { useCurrentTimeRange, includeTemplateVars, useShortUrl, selectedTheme } = this.state;
const shareUrl = await buildShareUrl(
useCurrentTimeRange,
includeTemplateVars,
selectedTheme.value,
panel,
useShortUrl
);
const imageUrl = buildImageUrl(useCurrentTimeRange, includeTemplateVars, selectedTheme.value, panel);
const { useCurrentTimeRange, useShortUrl, selectedTheme } = this.state;
const shareUrl = await buildShareUrl(useCurrentTimeRange, selectedTheme, panel, useShortUrl);
const imageUrl = buildImageUrl(useCurrentTimeRange, selectedTheme, panel);
this.setState({ shareUrl, imageUrl });
};
......@@ -76,15 +67,11 @@ export class ShareLink extends PureComponent<Props, State> {
this.setState({ useCurrentTimeRange: !this.state.useCurrentTimeRange });
};
onIncludeTemplateVarsChange = () => {
this.setState({ includeTemplateVars: !this.state.includeTemplateVars });
};
onUrlShorten = () => {
this.setState({ useShortUrl: !this.state.useShortUrl });
};
onThemeChange = (value: SelectableValue<string>) => {
onThemeChange = (value: string) => {
this.setState({ selectedTheme: value });
};
......@@ -98,60 +85,49 @@ export class ShareLink extends PureComponent<Props, State> {
render() {
const { panel } = this.props;
const { useCurrentTimeRange, includeTemplateVars, useShortUrl, selectedTheme, shareUrl, imageUrl } = this.state;
const isRelativeTime = this.props.dashboard ? this.props.dashboard.time.to === 'now' : false;
const { useCurrentTimeRange, useShortUrl, selectedTheme, shareUrl, imageUrl } = this.state;
const selectors = e2eSelectors.pages.SharePanelModal;
return (
<div className="share-modal-body">
<div className="share-modal-header">
<Icon name="link" className="share-modal-big-icon" size="xxl" />
<div className="share-modal-content">
<p className="share-modal-info-text">
Create a direct link to this dashboard or panel, customized with the options below.
</p>
<div className="gf-form-group">
<InlineField labelWidth={24} label="Current time range">
<FieldSet>
<Field
label="Lock time range"
description={
isRelativeTime ? 'Transforms the current relative time range to an absolute time range' : ''
}
>
<Switch
id="share-current-time-range"
value={useCurrentTimeRange}
onChange={this.onUseCurrentTimeRangeChange}
/>
</InlineField>
<InlineField labelWidth={24} label="Template variables">
<Switch
id="share-template-vars"
value={includeTemplateVars}
onChange={this.onIncludeTemplateVarsChange}
/>
</InlineField>
<InlineField labelWidth={24} label="Theme">
<Select width={20} options={themeOptions} value={selectedTheme} onChange={this.onThemeChange} />
</InlineField>
<InlineField labelWidth={24} label="Shorten URL">
</Field>
<Field label="Theme">
<RadioButtonGroup options={themeOptions} value={selectedTheme} onChange={this.onThemeChange} />
</Field>
<Field label="Shorten URL">
<Switch id="share-shorten-url" value={useShortUrl} onChange={this.onUrlShorten} />
</InlineField>
</div>
<div>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<Input
value={shareUrl}
readOnly
addonAfter={
<ClipboardButton
variant="primary"
getText={this.getShareUrl}
onClipboardCopy={this.onShareUrlCopy}
>
<Icon name="copy" /> Copy
</ClipboardButton>
}
/>
</div>
</div>
</div>
</div>
</Field>
<Field label="Link URL">
<Input
value={shareUrl}
readOnly
addonAfter={
<ClipboardButton variant="primary" getText={this.getShareUrl} onClipboardCopy={this.onShareUrlCopy}>
<Icon name="copy" /> Copy
</ClipboardButton>
}
/>
</Field>
</FieldSet>
{panel && config.rendererAvailable && (
<div className="gf-form">
<a href={imageUrl} target="_blank" rel="noreferrer" aria-label={selectors.linkToRenderedImage}>
......
import React, { PureComponent } from 'react';
import {
Button,
ClipboardButton,
Icon,
Spinner,
Select,
Input,
LinkButton,
InlineField,
InlineFieldRow,
} from '@grafana/ui';
import { Button, ClipboardButton, Icon, Spinner, Select, Input, LinkButton, Field } from '@grafana/ui';
import { AppEvents, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
......@@ -229,31 +219,26 @@ export class ShareSnapshot extends PureComponent<Props, State> {
URL. Share wisely.
</p>
</div>
<InlineFieldRow className="share-modal-options">
<InlineField labelWidth={24} label="Snapshot name">
<Input width={30} value={snapshotName} onChange={this.onSnapshotNameChange} />
</InlineField>
<InlineField labelWidth={24} label="Expire">
<Select width={30} options={expireOptions} value={selectedExpireOption} onChange={this.onExpireChange} />
</InlineField>
</InlineFieldRow>
<p className="share-modal-info-text">
You may need to configure the timeout value if it takes a long time to collect your dashboard&apos;s metrics.
</p>
<InlineFieldRow className="share-modal-options">
<InlineField labelWidth={24} label="Timeout (seconds)">
<Input type="number" width={21} value={timeoutSeconds} onChange={this.onTimeoutChange} />
</InlineField>
</InlineFieldRow>
<Field label="Snapshot name">
<Input width={30} value={snapshotName} onChange={this.onSnapshotNameChange} />
</Field>
<Field label="Expire">
<Select width={30} options={expireOptions} value={selectedExpireOption} onChange={this.onExpireChange} />
</Field>
<Field
label="Timeout (seconds)"
description="You may need to configure the timeout value if it takes a long time to collect your dashboard's
metrics."
>
<Input type="number" width={21} value={timeoutSeconds} onChange={this.onTimeoutChange} />
</Field>
<div className="gf-form-button-row">
<Button className="width-10" variant="primary" disabled={isLoading} onClick={this.createSnapshot()}>
<Button variant="primary" disabled={isLoading} onClick={this.createSnapshot()}>
Local Snapshot
</Button>
{externalEnabled && (
<Button className="width-16" variant="secondary" disabled={isLoading} onClick={this.createSnapshot(true)}>
<Button variant="secondary" disabled={isLoading} onClick={this.createSnapshot(true)}>
{sharingButtonText}
</Button>
)}
......@@ -309,17 +294,11 @@ export class ShareSnapshot extends PureComponent<Props, State> {
return (
<div className="share-modal-body">
<div className="share-modal-header">
{isLoading ? (
<div className="share-modal-big-icon">
<Spinner inline={true} />
</div>
) : (
<Icon name="camera" className="share-modal-big-icon" size="xxl" />
)}
<div className="share-modal-content">
{step === 1 && this.renderStep1()}
{step === 2 && this.renderStep2()}
{step === 3 && this.renderStep3()}
{isLoading && <Spinner inline={true} />}
</div>
</div>
</div>
......
......@@ -2,14 +2,8 @@ import { config } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { createShortLink } from 'app/core/utils/shortLinks';
import { PanelModel, dateTime, urlUtil } from '@grafana/data';
import { getAllVariableValuesForUrl } from 'app/features/variables/getAllVariableValuesForUrl';
export function buildParams(
useCurrentTimeRange: boolean,
includeTemplateVars: boolean,
selectedTheme?: string,
panel?: PanelModel
) {
export function buildParams(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
let params = urlUtil.getUrlSearchParams();
const range = getTimeSrv().timeRange();
......@@ -22,13 +16,6 @@ export function buildParams(
delete params.to;
}
if (includeTemplateVars) {
params = {
...params,
...getAllVariableValuesForUrl(),
};
}
if (selectedTheme !== 'current') {
params.theme = selectedTheme;
}
......@@ -55,13 +42,12 @@ export function buildBaseUrl() {
export async function buildShareUrl(
useCurrentTimeRange: boolean,
includeTemplateVars: boolean,
selectedTheme?: string,
panel?: PanelModel,
shortenUrl?: boolean
) {
const baseUrl = buildBaseUrl();
const params = buildParams(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel);
const params = buildParams(useCurrentTimeRange, selectedTheme, panel);
const shareUrl = urlUtil.appendQueryToUrl(baseUrl, urlUtil.toUrlParams(params));
if (shortenUrl) {
return await createShortLink(shareUrl);
......@@ -69,14 +55,9 @@ export async function buildShareUrl(
return shareUrl;
}
export function buildSoloUrl(
useCurrentTimeRange: boolean,
includeTemplateVars: boolean,
selectedTheme?: string,
panel?: PanelModel
) {
export function buildSoloUrl(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
const baseUrl = buildBaseUrl();
const params = buildParams(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel);
const params = buildParams(useCurrentTimeRange, selectedTheme, panel);
let soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
......@@ -88,13 +69,8 @@ export function buildSoloUrl(
return urlUtil.appendQueryToUrl(soloUrl, urlUtil.toUrlParams(params));
}
export function buildImageUrl(
useCurrentTimeRange: boolean,
includeTemplateVars: boolean,
selectedTheme?: string,
panel?: PanelModel
) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel);
export function buildImageUrl(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, selectedTheme, panel);
let imageUrl = soloUrl.replace(config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/');
imageUrl = imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
......@@ -102,13 +78,8 @@ export function buildImageUrl(
return imageUrl;
}
export function buildIframeHtml(
useCurrentTimeRange: boolean,
includeTemplateVars: boolean,
selectedTheme?: string,
panel?: PanelModel
) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel);
export function buildIframeHtml(useCurrentTimeRange: boolean, selectedTheme?: string, panel?: PanelModel) {
let soloUrl = buildSoloUrl(useCurrentTimeRange, selectedTheme, panel);
return '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
}
......
......@@ -127,12 +127,6 @@
}
.share-modal-body {
padding: 10px 0;
.tight-form {
text-align: left;
}
.share-modal-options {
margin: 11px 0px 33px 0px;
display: inline-block;
......@@ -160,10 +154,6 @@
flex-grow: 1;
}
.tight-form {
text-align: left;
}
.share-modal-link {
max-width: 716px;
white-space: nowrap;
......
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