Co-authored-by: kay delaney <kay@grafana.com> Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
This commit is contained in:
co-authored by
kay delaney
Alexandra Vargas
parent
b8f9d46328
commit
7880990c5f
@@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { HorizontalGroup, Input } from '..';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Button } from '../Button';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
|
||||
export interface ConfirmModalProps {
|
||||
@@ -21,8 +21,12 @@ export interface ConfirmModalProps {
|
||||
description?: React.ReactNode;
|
||||
/** Text for confirm button */
|
||||
confirmText: string;
|
||||
/** Variant for confirm button */
|
||||
confirmVariant?: ButtonVariant;
|
||||
/** Text for dismiss button */
|
||||
dismissText?: string;
|
||||
/** Variant for dismiss button */
|
||||
dismissVariant?: ButtonVariant;
|
||||
/** Icon for the modal header */
|
||||
icon?: IconName;
|
||||
/** Text user needs to fill in before confirming */
|
||||
@@ -43,8 +47,10 @@ export const ConfirmModal = ({
|
||||
body,
|
||||
description,
|
||||
confirmText,
|
||||
confirmVariant = 'destructive',
|
||||
confirmationText,
|
||||
dismissText = 'Cancel',
|
||||
dismissVariant = 'secondary',
|
||||
alternativeText,
|
||||
icon = 'exclamation-triangle',
|
||||
onConfirm,
|
||||
@@ -79,11 +85,11 @@ export const ConfirmModal = ({
|
||||
) : null}
|
||||
</div>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
||||
<Button variant={dismissVariant} onClick={onDismiss} fill="outline">
|
||||
{dismissText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant={confirmVariant}
|
||||
onClick={onConfirm}
|
||||
disabled={disabled}
|
||||
ref={buttonRef}
|
||||
|
||||
@@ -75,7 +75,26 @@ func createExternalDashboardSnapshot(cmd models.CreateDashboardSnapshotCommand)
|
||||
return &createSnapshotResponse, nil
|
||||
}
|
||||
|
||||
// POST /api/snapshots
|
||||
func createOriginalDashboardURL(appURL string, cmd *models.CreateDashboardSnapshotCommand) (string, error) {
|
||||
dashUID := cmd.Dashboard.Get("uid").MustString("")
|
||||
if ok := util.IsValidShortUID(dashUID); !ok {
|
||||
return "", fmt.Errorf("invalid dashboard UID")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/d/%v", dashUID), nil
|
||||
}
|
||||
|
||||
// swagger:route POST /snapshots snapshots createDashboardSnapshot
|
||||
//
|
||||
// When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI.
|
||||
//
|
||||
// Snapshot public mode should be enabled or authentication is required.
|
||||
//
|
||||
// Responses:
|
||||
// 200: createDashboardSnapshotResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Response {
|
||||
cmd := models.CreateDashboardSnapshotCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
@@ -85,10 +104,14 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
cmd.Name = "Unnamed snapshot"
|
||||
}
|
||||
|
||||
var url string
|
||||
var snapshotUrl string
|
||||
cmd.ExternalUrl = ""
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.UserId = c.UserId
|
||||
originalDashboardURL, err := createOriginalDashboardURL(hs.Cfg.AppURL, &cmd)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Invalid app URL", err)
|
||||
}
|
||||
|
||||
if cmd.External {
|
||||
if !setting.ExternalEnabled {
|
||||
@@ -102,7 +125,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
return nil
|
||||
}
|
||||
|
||||
url = response.Url
|
||||
snapshotUrl = response.Url
|
||||
cmd.Key = response.Key
|
||||
cmd.DeleteKey = response.DeleteKey
|
||||
cmd.ExternalUrl = response.Url
|
||||
@@ -111,6 +134,8 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
|
||||
metrics.MApiDashboardSnapshotExternal.Inc()
|
||||
} else {
|
||||
cmd.Dashboard.SetPath([]string{"snapshot", "originalUrl"}, originalDashboardURL)
|
||||
|
||||
if cmd.Key == "" {
|
||||
var err error
|
||||
cmd.Key, err = util.GetRandomString(32)
|
||||
@@ -129,7 +154,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
}
|
||||
}
|
||||
|
||||
url = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
|
||||
snapshotUrl = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
|
||||
|
||||
metrics.MApiDashboardSnapshotCreate.Inc()
|
||||
}
|
||||
@@ -142,7 +167,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
c.JSON(200, util.DynMap{
|
||||
"key": cmd.Key,
|
||||
"deleteKey": cmd.DeleteKey,
|
||||
"url": url,
|
||||
"url": snapshotUrl,
|
||||
"deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey),
|
||||
"id": cmd.Result.Id,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { locationUtil, textUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar, useForceUpdate } from '@grafana/ui';
|
||||
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar, useForceUpdate, ConfirmModal } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { toggleKioskMode } from 'app/core/navigation/kiosk';
|
||||
import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal';
|
||||
import { SaveDashboardProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardProxy';
|
||||
@@ -13,6 +16,7 @@ import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { KioskMode } from 'app/types';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { getDashboardSrv } from '../../services/DashboardSrv';
|
||||
import { DashboardModel } from '../../state';
|
||||
@@ -58,6 +62,45 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
export const DashNav = React.memo<Props>((props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const originalUrl = props.dashboard.snapshot?.originalUrl ?? '';
|
||||
const gotoSnapshotOrigin = () => {
|
||||
window.location.href = textUtil.sanitizeUrl(props.dashboard.snapshot.originalUrl);
|
||||
};
|
||||
|
||||
const notifyApp = useAppNotification();
|
||||
const onOpenSnapshotOriginal = () => {
|
||||
try {
|
||||
const sanitizedUrl = new URL(textUtil.sanitizeUrl(originalUrl), config.appUrl);
|
||||
const appUrl = new URL(config.appUrl);
|
||||
if (sanitizedUrl.host !== appUrl.host) {
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: ConfirmModal,
|
||||
props: {
|
||||
title: 'Proceed to external site?',
|
||||
modalClass: modalStyles,
|
||||
body: (
|
||||
<>
|
||||
<p>
|
||||
{`This link connects to an external website at`} <code>{originalUrl}</code>
|
||||
</p>
|
||||
<p>{"Are you sure you'd like to proceed?"}</p>
|
||||
</>
|
||||
),
|
||||
confirmVariant: 'primary',
|
||||
confirmText: 'Proceed',
|
||||
onConfirm: gotoSnapshotOrigin,
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
gotoSnapshotOrigin();
|
||||
}
|
||||
} catch (err) {
|
||||
notifyApp.error('Invalid URL', err instanceof Error ? err.message : undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onStarDashboard = () => {
|
||||
const dashboardSrv = getDashboardSrv();
|
||||
const { dashboard } = props;
|
||||
@@ -240,7 +283,7 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
buttons.push(
|
||||
<ToolbarButton
|
||||
tooltip="Open original dashboard"
|
||||
onClick={() => gotoSnapshotOrigin(snapshotUrl)}
|
||||
onClick={onOpenSnapshotOriginal}
|
||||
icon="link"
|
||||
key="button-snapshot"
|
||||
/>
|
||||
@@ -260,10 +303,6 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
return buttons;
|
||||
};
|
||||
|
||||
const gotoSnapshotOrigin = (snapshotUrl: string) => {
|
||||
window.location.href = textUtil.sanitizeUrl(snapshotUrl);
|
||||
};
|
||||
|
||||
const { isFullscreen, title, folderTitle } = props;
|
||||
// this ensures the component rerenders when the location changes
|
||||
const location = useLocation();
|
||||
@@ -289,3 +328,8 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
DashNav.displayName = 'DashNav';
|
||||
|
||||
export default connector(DashNav);
|
||||
|
||||
const modalStyles = css({
|
||||
width: 'max-content',
|
||||
maxWidth: '80vw',
|
||||
});
|
||||
|
||||
@@ -73,10 +73,6 @@ export class ShareSnapshot extends PureComponent<Props, State> {
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
if (!external) {
|
||||
this.dashboard.snapshot.originalUrl = window.location.href;
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
this.dashboard.startRefresh();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user