mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SharePanelInternally: Add render image inside drawer with customization (#97179)
This commit is contained in:
parent
62b326a08c
commit
2c3c06e00f
@ -697,6 +697,26 @@ export const versionedPages = {
|
||||
copyUrlButton: {
|
||||
'11.3.0': 'data-testid share internally copy url button',
|
||||
},
|
||||
SharePanel: {
|
||||
preview: {
|
||||
'11.5.0': 'data-testid share panel internally image generation preview',
|
||||
},
|
||||
widthInput: {
|
||||
'11.5.0': 'data-testid share panel internally width input',
|
||||
},
|
||||
heightInput: {
|
||||
'11.5.0': 'data-testid share panel internally height input',
|
||||
},
|
||||
scaleFactorInput: {
|
||||
'11.5.0': 'data-testid share panel internally scale factor input',
|
||||
},
|
||||
generateImageButton: {
|
||||
'11.5.0': 'data-testid share panel internally generate image button',
|
||||
},
|
||||
downloadImageButton: {
|
||||
'11.5.0': 'data-testid share panel internally download image button',
|
||||
},
|
||||
},
|
||||
},
|
||||
ShareExternally: {
|
||||
container: {
|
||||
|
@ -19,7 +19,7 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { getCreateAlertInMenuAvailability } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
|
||||
import { shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
|
||||
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
@ -90,6 +90,11 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
iconClassName: 'link',
|
||||
shortcut: 'p u',
|
||||
onClick: () => {
|
||||
DashboardInteractions.sharingCategoryClicked({
|
||||
item: shareDashboardType.link,
|
||||
shareResource: getTrackingSource(panel?.getRef()),
|
||||
});
|
||||
|
||||
const drawer = new ShareDrawer({
|
||||
shareView: shareDashboardType.link,
|
||||
panelRef: panel.getRef(),
|
||||
@ -103,6 +108,11 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
iconClassName: 'arrow',
|
||||
shortcut: 'p e',
|
||||
onClick: () => {
|
||||
DashboardInteractions.sharingCategoryClicked({
|
||||
item: shareDashboardType.embed,
|
||||
shareResource: getTrackingSource(panel.getRef()),
|
||||
});
|
||||
|
||||
const drawer = new ShareDrawer({
|
||||
shareView: shareDashboardType.embed,
|
||||
panelRef: panel.getRef(),
|
||||
@ -122,6 +132,11 @@ export function panelMenuBehavior(menu: VizPanelMenu, isRepeat = false) {
|
||||
iconClassName: 'camera',
|
||||
shortcut: 'p s',
|
||||
onClick: () => {
|
||||
DashboardInteractions.sharingCategoryClicked({
|
||||
item: shareDashboardType.snapshot,
|
||||
shareResource: getTrackingSource(panel.getRef()),
|
||||
});
|
||||
|
||||
const drawer = new ShareDrawer({
|
||||
shareView: shareDashboardType.snapshot,
|
||||
panelRef: panel.getRef(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { dateTime, UrlQueryMap } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectRef, VizPanel, sceneGraph } from '@grafana/scenes';
|
||||
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { Alert, ClipboardButton, Field, FieldSet, Icon, Input, Switch } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
@ -14,6 +14,7 @@ import { getDashboardUrl } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { SceneShareTabState, ShareView } from './types';
|
||||
|
||||
export interface ShareLinkTabState extends SceneShareTabState, ShareOptions {
|
||||
panelRef?: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
@ -55,7 +56,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> implements
|
||||
this.onThemeChange = this.onThemeChange.bind(this);
|
||||
}
|
||||
|
||||
async buildUrl() {
|
||||
buildUrl = async (queryOptions?: UrlQueryMap) => {
|
||||
this.setState({ isBuildUrlLoading: true });
|
||||
const { panelRef, useLockedTime: useAbsoluteTimeRange, useShortUrl, selectedTheme } = this.state;
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
@ -83,7 +84,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> implements
|
||||
const imageUrl = getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
currentQueryParams: location.search,
|
||||
updateQuery: { ...urlParamsUpdate, panelId: panel?.state.key },
|
||||
updateQuery: { ...urlParamsUpdate, ...queryOptions, panelId: panel?.state.key },
|
||||
absolute: true,
|
||||
soloRoute: true,
|
||||
render: true,
|
||||
@ -91,7 +92,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> implements
|
||||
});
|
||||
|
||||
this.setState({ shareUrl, imageUrl, isBuildUrlLoading: false });
|
||||
}
|
||||
};
|
||||
|
||||
public getTabLabel() {
|
||||
return t('share-modal.tab-title.link', 'Link');
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { config, setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { userEvent } from '../../../../../test/test-utils';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { activateFullSceneTree } from '../../utils/test-utils';
|
||||
|
||||
import { SharePanelInternally } from './SharePanelInternally';
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
const selector = e2eSelectors.pages.ShareDashboardDrawer.ShareInternally.SharePanel;
|
||||
|
||||
describe('SharePanelInternally', () => {
|
||||
it('should disable all image generation inputs when renderer is not available', async () => {
|
||||
config.rendererAvailable = false;
|
||||
buildAndRenderScenario();
|
||||
|
||||
expect(await screen.findByTestId(selector.preview)).toBeInTheDocument();
|
||||
[
|
||||
selector.widthInput,
|
||||
selector.heightInput,
|
||||
selector.scaleFactorInput,
|
||||
selector.generateImageButton,
|
||||
selector.downloadImageButton,
|
||||
].forEach((selector) => {
|
||||
expect(screen.getByTestId(selector)).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable all image generation inputs when renderer is available', async () => {
|
||||
config.rendererAvailable = true;
|
||||
buildAndRenderScenario();
|
||||
|
||||
expect(await screen.findByTestId(selector.preview)).toBeInTheDocument();
|
||||
[selector.widthInput, selector.heightInput, selector.scaleFactorInput].forEach((selector) => {
|
||||
expect(screen.getByTestId(selector)).toBeEnabled();
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByTestId(selector.widthInput), '1000');
|
||||
await userEvent.type(screen.getByTestId(selector.widthInput), '2000');
|
||||
expect(screen.getByTestId(selector.generateImageButton)).toBeEnabled();
|
||||
expect(screen.getByTestId(selector.downloadImageButton)).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
function buildAndRenderScenario() {
|
||||
const panel = new VizPanel({
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-12',
|
||||
});
|
||||
const tab = new SharePanelInternally({ panelRef: panel.getRef() });
|
||||
const scene = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
body: DefaultGridLayoutManager.fromVizPanels([panel]),
|
||||
overlay: tab,
|
||||
});
|
||||
|
||||
activateFullSceneTree(scene);
|
||||
|
||||
render(<tab.Component model={tab} />);
|
||||
|
||||
return tab;
|
||||
}
|
@ -3,13 +3,15 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { Alert, ClipboardButton, Divider, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, ClipboardButton, Divider, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import ShareInternallyConfiguration from '../ShareInternallyConfiguration';
|
||||
import { ShareLinkTab, ShareLinkTabState } from '../ShareLinkTab';
|
||||
|
||||
import { SharePanelPreview } from './SharePanelPreview';
|
||||
|
||||
export class SharePanelInternally extends ShareLinkTab {
|
||||
static Component = SharePanelInternallyRenderer;
|
||||
|
||||
@ -24,56 +26,44 @@ export class SharePanelInternally extends ShareLinkTab {
|
||||
|
||||
function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelInternally>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { useLockedTime, useShortUrl, selectedTheme, isBuildUrlLoading, imageUrl } = model.useState();
|
||||
|
||||
const { useLockedTime, useShortUrl, selectedTheme, isBuildUrlLoading, imageUrl, panelRef } = model.useState();
|
||||
|
||||
const panelTitle = panelRef?.resolve().state.title;
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const isDashboardSaved = Boolean(dashboard.state.uid);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.configDescription}>
|
||||
<Text variant="body">
|
||||
<Trans i18nKey="link.share-panel.config-description">
|
||||
Create a personalized, direct link to share your panel within your organization, with the following
|
||||
customization settings:
|
||||
</Trans>
|
||||
</Text>
|
||||
<div>
|
||||
<Text variant="body">
|
||||
<Trans i18nKey="link.share-panel.config-description">
|
||||
Create a personalized, direct link to share your panel within your organization, with the following
|
||||
customization settings:
|
||||
</Trans>
|
||||
</Text>
|
||||
<div className={styles.configurationContainer}>
|
||||
<ShareInternallyConfiguration
|
||||
useLockedTime={useLockedTime}
|
||||
onToggleLockedTime={model.onToggleLockedTime}
|
||||
useShortUrl={useShortUrl}
|
||||
onUrlShorten={model.onUrlShorten}
|
||||
selectedTheme={selectedTheme}
|
||||
onChangeTheme={model.onThemeChange}
|
||||
isLoading={isBuildUrlLoading}
|
||||
/>
|
||||
<ClipboardButton
|
||||
icon="link"
|
||||
variant="primary"
|
||||
fill="outline"
|
||||
disabled={isBuildUrlLoading}
|
||||
getText={model.getShareUrl}
|
||||
onClipboardCopy={model.onCopy}
|
||||
>
|
||||
<Trans i18nKey="link.share.copy-link-button">Copy link</Trans>
|
||||
</ClipboardButton>
|
||||
</div>
|
||||
<ShareInternallyConfiguration
|
||||
useLockedTime={useLockedTime}
|
||||
onToggleLockedTime={() => model.onToggleLockedTime()}
|
||||
useShortUrl={useShortUrl}
|
||||
onUrlShorten={() => model.onUrlShorten()}
|
||||
selectedTheme={selectedTheme}
|
||||
onChangeTheme={(t) => model.onThemeChange(t)}
|
||||
isLoading={isBuildUrlLoading}
|
||||
/>
|
||||
<Divider spacing={1} />
|
||||
<Divider spacing={2} />
|
||||
<Stack gap={2} direction="column">
|
||||
<div className={styles.buttonsContainer}>
|
||||
<Stack gap={1} flex={1} direction={{ xs: 'column', sm: 'row' }}>
|
||||
<ClipboardButton
|
||||
icon="link"
|
||||
variant="primary"
|
||||
fill="outline"
|
||||
disabled={isBuildUrlLoading}
|
||||
getText={model.getShareUrl}
|
||||
onClipboardCopy={model.onCopy}
|
||||
>
|
||||
<Trans i18nKey="link.share.copy-link-button">Copy link</Trans>
|
||||
</ClipboardButton>
|
||||
<LinkButton
|
||||
href={imageUrl}
|
||||
icon="external-link-alt"
|
||||
target="_blank"
|
||||
variant="secondary"
|
||||
fill="solid"
|
||||
disabled={!config.rendererAvailable || !isDashboardSaved}
|
||||
>
|
||||
<Trans i18nKey="link.share-panel.render-image">Render image</Trans>
|
||||
</LinkButton>
|
||||
</Stack>
|
||||
</div>
|
||||
{!isDashboardSaved && (
|
||||
<Alert severity="info" title={t('share-modal.link.save-alert', 'Dashboard is not saved')} bottomSpacing={0}>
|
||||
<Trans i18nKey="share-modal.link.save-dashboard">
|
||||
@ -101,16 +91,20 @@ function SharePanelInternallyRenderer({ model }: SceneComponentProps<SharePanelI
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
<SharePanelPreview
|
||||
title={panelTitle || ''}
|
||||
buildUrl={model.buildUrl}
|
||||
imageUrl={imageUrl}
|
||||
disabled={!isDashboardSaved}
|
||||
theme={selectedTheme}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
configDescription: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
buttonsContainer: css({
|
||||
configurationContainer: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
@ -0,0 +1,228 @@
|
||||
import { css } from '@emotion/css';
|
||||
import saveAs from 'file-saver';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { GrafanaTheme2, UrlQueryMap } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { config, getBackendSrv, isFetchError } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, FieldSet, Icon, Input, LoadingBar, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
|
||||
type ImageSettingsForm = {
|
||||
width: number;
|
||||
height: number;
|
||||
scaleFactor: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
buildUrl: (urlParams: UrlQueryMap) => void;
|
||||
imageUrl: string;
|
||||
disabled: boolean;
|
||||
theme: string;
|
||||
};
|
||||
|
||||
const selector = e2eSelectors.pages.ShareDashboardDrawer.ShareInternally.SharePanel;
|
||||
|
||||
export function SharePanelPreview({ title, imageUrl, buildUrl, disabled, theme }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
watch,
|
||||
formState: { errors, isValid },
|
||||
} = useForm<ImageSettingsForm>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
width: 1000,
|
||||
height: 500,
|
||||
scaleFactor: 1,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
buildUrl({ width: watch('width'), height: watch('height'), scale: watch('scaleFactor') });
|
||||
}, [buildUrl, watch]);
|
||||
|
||||
const [{ loading, value: image, error }, renderImage] = useAsyncFn(async () => {
|
||||
const { width, height, scaleFactor } = watch();
|
||||
DashboardInteractions.generatePanelImageClicked({
|
||||
width,
|
||||
height,
|
||||
scaleFactor,
|
||||
theme,
|
||||
shareResource: 'panel',
|
||||
});
|
||||
const response = await lastValueFrom(getBackendSrv().fetch<BlobPart>({ url: imageUrl, responseType: 'blob' }));
|
||||
return new Blob([response.data], { type: 'image/png' });
|
||||
}, [imageUrl, watch('width'), watch('height'), watch('scaleFactor'), theme]);
|
||||
|
||||
const onDownloadImageClick = () => {
|
||||
DashboardInteractions.downloadPanelImageClicked({ shareResource: 'panel' });
|
||||
saveAs(image!, `${title}.png`);
|
||||
};
|
||||
|
||||
const onChange = () => {
|
||||
buildUrl({ width: watch('width'), height: watch('height'), scale: watch('scaleFactor') });
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid={selector.preview}>
|
||||
<Stack gap={2} direction="column">
|
||||
<Text element="h4">
|
||||
<Trans i18nKey="share-panel-image.preview.title">Panel preview</Trans>
|
||||
</Text>
|
||||
<form onSubmit={handleSubmit(renderImage)}>
|
||||
<FieldSet
|
||||
disabled={!config.rendererAvailable}
|
||||
label={
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Text element="h5">
|
||||
<Trans i18nKey="share-panel-image.settings.title">Image settings</Trans>
|
||||
</Text>
|
||||
<Tooltip
|
||||
content={t(
|
||||
'share-panel-image.settings.max-warning',
|
||||
'Setting maximums are limited by the image renderer service'
|
||||
)}
|
||||
>
|
||||
<Icon name="info-circle" size="sm" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack gap={1} justifyContent="space-between" direction={{ xs: 'column', sm: 'row' }}>
|
||||
<Field
|
||||
label={t('share-panel-image.settings.width-label', 'Width')}
|
||||
className={styles.imageConfigurationField}
|
||||
required
|
||||
invalid={!!errors.width}
|
||||
error={errors.width?.message}
|
||||
>
|
||||
<Input
|
||||
{...register('width', {
|
||||
required: t('share-panel-image.settings.width-required', 'Width is required'),
|
||||
min: {
|
||||
value: 1,
|
||||
message: t('share-panel-image.settings.width-min', 'Width must be equal or greater than 1'),
|
||||
},
|
||||
onChange: onChange,
|
||||
})}
|
||||
placeholder={t('share-panel-image.settings.width-placeholder', '1000')}
|
||||
type="number"
|
||||
suffix="px"
|
||||
data-testid={selector.widthInput}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('share-panel-image.settings.height-label', 'Height')}
|
||||
className={styles.imageConfigurationField}
|
||||
required
|
||||
invalid={!!errors.height}
|
||||
error={errors.height?.message}
|
||||
>
|
||||
<Input
|
||||
{...register('height', {
|
||||
required: t('share-panel-image.settings.height-required', 'Height is required'),
|
||||
min: {
|
||||
value: 1,
|
||||
message: t('share-panel-image.settings.height-min', 'Height must be equal or greater than 1'),
|
||||
},
|
||||
onChange: onChange,
|
||||
})}
|
||||
placeholder={t('share-panel-image.settings.height-placeholder', '500')}
|
||||
type="number"
|
||||
suffix="px"
|
||||
data-testid={selector.heightInput}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('share-panel-image.settings.scale-factor-label', 'Scale factor')}
|
||||
className={styles.imageConfigurationField}
|
||||
required
|
||||
invalid={!!errors.scaleFactor}
|
||||
error={errors.scaleFactor?.message}
|
||||
>
|
||||
<Input
|
||||
{...register('scaleFactor', {
|
||||
required: t('share-panel-image.settings.scale-factor-required', 'Scale factor is required'),
|
||||
min: {
|
||||
value: 1,
|
||||
message: t(
|
||||
'share-panel-image.settings.scale-factor-min',
|
||||
'Scale factor must be equal or greater than 1'
|
||||
),
|
||||
},
|
||||
onChange: onChange,
|
||||
})}
|
||||
placeholder={t('share-panel-image.settings.scale-factor-placeholder', '1')}
|
||||
type="number"
|
||||
data-testid={selector.scaleFactorInput}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
<Stack gap={1} direction={{ xs: 'column', sm: 'row' }}>
|
||||
<Button
|
||||
icon="gf-layout-simple"
|
||||
variant="secondary"
|
||||
fill="solid"
|
||||
type="submit"
|
||||
disabled={disabled || loading || !isValid}
|
||||
data-testid={selector.generateImageButton}
|
||||
>
|
||||
<Trans i18nKey="link.share-panel.render-image">Generate image</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDownloadImageClick}
|
||||
icon={'download-alt'}
|
||||
variant="secondary"
|
||||
disabled={!image || loading || disabled}
|
||||
data-testid={selector.downloadImageButton}
|
||||
>
|
||||
<Trans i18nKey="link.share-panel.download-image">Download image</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
</form>
|
||||
{loading && (
|
||||
<div>
|
||||
<LoadingBar width={128} />
|
||||
<div className={styles.imageLoadingContainer}>
|
||||
<Text variant="body">{title || ''}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{image && !loading && <img src={URL.createObjectURL(image)} alt="panel-preview-img" className={styles.image} />}
|
||||
{error && !loading && (
|
||||
<Alert severity="error" title={t('link.share-panel.render-image-error', 'Failed to render panel image')}>
|
||||
{isFetchError(error)
|
||||
? error.statusText
|
||||
: t('link.share-panel.render-image-error-description', 'An error occurred when generating the image')}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
imageConfigurationField: css({
|
||||
flex: 1,
|
||||
}),
|
||||
image: css({
|
||||
maxWidth: '100%',
|
||||
width: 'max-content',
|
||||
}),
|
||||
imageLoadingContainer: css({
|
||||
maxWidth: '100%',
|
||||
height: 362,
|
||||
border: `1px solid ${theme.components.input.borderColor}`,
|
||||
padding: theme.spacing(1),
|
||||
}),
|
||||
});
|
@ -45,6 +45,12 @@ export const DashboardInteractions = {
|
||||
embedSnippetCopy: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('sharing_embed_copy_clicked', properties);
|
||||
},
|
||||
generatePanelImageClicked: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('sharing_link_generate_image_clicked', properties);
|
||||
},
|
||||
downloadPanelImageClicked: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('sharing_link_download_image_clicked', properties);
|
||||
},
|
||||
publishSnapshotClicked: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('sharing_snapshot_publish_clicked', properties);
|
||||
},
|
||||
|
@ -50,8 +50,8 @@ export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
|
||||
options.updateQuery = {
|
||||
...options.updateQuery,
|
||||
width: 1000,
|
||||
height: 500,
|
||||
width: options.updateQuery?.width || 1000,
|
||||
height: options.updateQuery?.height || 500,
|
||||
tz: options.timeZone,
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { FormEvent, useEffect, useState } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { RawTimeRange, TimeRange } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, ClipboardButton, Field, Label, Modal, Stack, Switch, TextArea } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
@ -24,10 +23,6 @@ export function ShareEmbed({ panel, dashboard, range, onCancelClick, buildIframe
|
||||
const [selectedTheme, setSelectedTheme] = useState('current');
|
||||
const [iframeHtml, setIframeHtml] = useState('');
|
||||
|
||||
useEffectOnce(() => {
|
||||
reportInteraction('grafana_dashboards_embed_share_viewed', { shareResource: getTrackingSource(panel) });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newIframeHtml = buildIframe(useCurrentTimeRange, dashboard.uid, selectedTheme, panel, range);
|
||||
setIframeHtml(newIframeHtml);
|
||||
|
@ -1715,7 +1715,10 @@
|
||||
},
|
||||
"share-panel": {
|
||||
"config-description": "Create a personalized, direct link to share your panel within your organization, with the following customization settings:",
|
||||
"render-image": "Render image"
|
||||
"download-image": "Download image",
|
||||
"render-image": "Generate image",
|
||||
"render-image-error": "Failed to render panel image",
|
||||
"render-image-error-description": "An error occurred when generating the image"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@ -3033,6 +3036,27 @@
|
||||
"create-button": "Create library panel"
|
||||
}
|
||||
},
|
||||
"share-panel-image": {
|
||||
"preview": {
|
||||
"title": "Panel preview"
|
||||
},
|
||||
"settings": {
|
||||
"height-label": "Height",
|
||||
"height-min": "Height must be equal or greater than 1",
|
||||
"height-placeholder": "500",
|
||||
"height-required": "Height is required",
|
||||
"max-warning": "Setting maximums are limited by the image renderer service",
|
||||
"scale-factor-label": "Scale factor",
|
||||
"scale-factor-min": "Scale factor must be equal or greater than 1",
|
||||
"scale-factor-placeholder": "1",
|
||||
"scale-factor-required": "Scale factor is required",
|
||||
"title": "Image settings",
|
||||
"width-label": "Width",
|
||||
"width-min": "Width must be equal or greater than 1",
|
||||
"width-placeholder": "1000",
|
||||
"width-required": "Width is required"
|
||||
}
|
||||
},
|
||||
"share-playlist": {
|
||||
"checkbox-description": "Panel heights will be adjusted to fit screen size",
|
||||
"checkbox-label": "Autofit",
|
||||
|
@ -1715,7 +1715,10 @@
|
||||
},
|
||||
"share-panel": {
|
||||
"config-description": "Cřęäŧę ä pęřşőʼnäľįžęđ, đįřęčŧ ľįʼnĸ ŧő şĥäřę yőūř päʼnęľ ŵįŧĥįʼn yőūř őřģäʼnįžäŧįőʼn, ŵįŧĥ ŧĥę ƒőľľőŵįʼnģ čūşŧőmįžäŧįőʼn şęŧŧįʼnģş:",
|
||||
"render-image": "Ŗęʼnđęř įmäģę"
|
||||
"download-image": "Đőŵʼnľőäđ įmäģę",
|
||||
"render-image": "Ğęʼnęřäŧę įmäģę",
|
||||
"render-image-error": "Fäįľęđ ŧő řęʼnđęř päʼnęľ įmäģę",
|
||||
"render-image-error-description": "Åʼn ęřřőř őččūřřęđ ŵĥęʼn ģęʼnęřäŧįʼnģ ŧĥę įmäģę"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@ -3033,6 +3036,27 @@
|
||||
"create-button": "Cřęäŧę ľįþřäřy päʼnęľ"
|
||||
}
|
||||
},
|
||||
"share-panel-image": {
|
||||
"preview": {
|
||||
"title": "Päʼnęľ přęvįęŵ"
|
||||
},
|
||||
"settings": {
|
||||
"height-label": "Ħęįģĥŧ",
|
||||
"height-min": "Ħęįģĥŧ mūşŧ þę ęqūäľ őř ģřęäŧęř ŧĥäʼn 1",
|
||||
"height-placeholder": "500",
|
||||
"height-required": "Ħęįģĥŧ įş řęqūįřęđ",
|
||||
"max-warning": "Ŝęŧŧįʼnģ mäχįmūmş äřę ľįmįŧęđ þy ŧĥę įmäģę řęʼnđęřęř şęřvįčę",
|
||||
"scale-factor-label": "Ŝčäľę ƒäčŧőř",
|
||||
"scale-factor-min": "Ŝčäľę ƒäčŧőř mūşŧ þę ęqūäľ őř ģřęäŧęř ŧĥäʼn 1",
|
||||
"scale-factor-placeholder": "1",
|
||||
"scale-factor-required": "Ŝčäľę ƒäčŧőř įş řęqūįřęđ",
|
||||
"title": "Ĩmäģę şęŧŧįʼnģş",
|
||||
"width-label": "Ŵįđŧĥ",
|
||||
"width-min": "Ŵįđŧĥ mūşŧ þę ęqūäľ őř ģřęäŧęř ŧĥäʼn 1",
|
||||
"width-placeholder": "1000",
|
||||
"width-required": "Ŵįđŧĥ įş řęqūįřęđ"
|
||||
}
|
||||
},
|
||||
"share-playlist": {
|
||||
"checkbox-description": "Päʼnęľ ĥęįģĥŧş ŵįľľ þę äđĵūşŧęđ ŧő ƒįŧ şčřęęʼn şįžę",
|
||||
"checkbox-label": "Åūŧőƒįŧ",
|
||||
|
Loading…
Reference in New Issue
Block a user