ShareSnapshot.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import React, { PureComponent } from 'react';
  2. import { AppEvents, SelectableValue } from '@grafana/data';
  3. import { getBackendSrv, reportInteraction } from '@grafana/runtime';
  4. import { Button, ClipboardButton, Field, Icon, Input, LinkButton, Modal, Select, Spinner } from '@grafana/ui';
  5. import { appEvents } from 'app/core/core';
  6. import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
  7. import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
  8. import { VariableRefresh } from '../../../variables/types';
  9. import { ShareModalTabProps } from './types';
  10. const snapshotApiUrl = '/api/snapshots';
  11. const expireOptions: Array<SelectableValue<number>> = [
  12. { label: 'Never', value: 0 },
  13. { label: '1 Hour', value: 60 * 60 },
  14. { label: '1 Day', value: 60 * 60 * 24 },
  15. { label: '7 Days', value: 60 * 60 * 24 * 7 },
  16. ];
  17. interface Props extends ShareModalTabProps {}
  18. interface State {
  19. isLoading: boolean;
  20. step: number;
  21. snapshotName: string;
  22. selectedExpireOption: SelectableValue<number>;
  23. snapshotExpires?: number;
  24. snapshotUrl: string;
  25. deleteUrl: string;
  26. timeoutSeconds: number;
  27. externalEnabled: boolean;
  28. sharingButtonText: string;
  29. }
  30. export class ShareSnapshot extends PureComponent<Props, State> {
  31. private dashboard: DashboardModel;
  32. constructor(props: Props) {
  33. super(props);
  34. this.dashboard = props.dashboard;
  35. this.state = {
  36. isLoading: false,
  37. step: 1,
  38. selectedExpireOption: expireOptions[0],
  39. snapshotExpires: expireOptions[0].value,
  40. snapshotName: props.dashboard.title,
  41. timeoutSeconds: 4,
  42. snapshotUrl: '',
  43. deleteUrl: '',
  44. externalEnabled: false,
  45. sharingButtonText: '',
  46. };
  47. }
  48. componentDidMount() {
  49. this.getSnaphotShareOptions();
  50. }
  51. async getSnaphotShareOptions() {
  52. const shareOptions = await getBackendSrv().get('/api/snapshot/shared-options');
  53. this.setState({
  54. sharingButtonText: shareOptions['externalSnapshotName'],
  55. externalEnabled: shareOptions['externalEnabled'],
  56. });
  57. }
  58. createSnapshot = (external?: boolean) => () => {
  59. const { timeoutSeconds } = this.state;
  60. this.dashboard.snapshot = {
  61. timestamp: new Date(),
  62. };
  63. if (!external) {
  64. this.dashboard.snapshot.originalUrl = window.location.href;
  65. }
  66. this.setState({ isLoading: true });
  67. this.dashboard.startRefresh();
  68. setTimeout(() => {
  69. this.saveSnapshot(this.dashboard, external);
  70. }, timeoutSeconds * 1000);
  71. };
  72. saveSnapshot = async (dashboard: DashboardModel, external?: boolean) => {
  73. const { snapshotExpires } = this.state;
  74. const dash = this.dashboard.getSaveModelClone();
  75. this.scrubDashboard(dash);
  76. const cmdData = {
  77. dashboard: dash,
  78. name: dash.title,
  79. expires: snapshotExpires,
  80. external: external,
  81. };
  82. try {
  83. const results: { deleteUrl: string; url: string } = await getBackendSrv().post(snapshotApiUrl, cmdData);
  84. this.setState({
  85. deleteUrl: results.deleteUrl,
  86. snapshotUrl: results.url,
  87. step: 2,
  88. });
  89. } finally {
  90. reportInteraction('grafana_dashboards_snapshot_created', {
  91. location: external ? 'raintank' : 'local',
  92. });
  93. this.setState({ isLoading: false });
  94. }
  95. };
  96. scrubDashboard = (dash: DashboardModel) => {
  97. const { panel } = this.props;
  98. const { snapshotName } = this.state;
  99. // change title
  100. dash.title = snapshotName;
  101. // make relative times absolute
  102. dash.time = getTimeSrv().timeRange();
  103. // Remove links
  104. dash.links = [];
  105. // remove panel queries & links
  106. dash.panels.forEach((panel) => {
  107. panel.targets = [];
  108. panel.links = [];
  109. panel.datasource = null;
  110. });
  111. // remove annotation queries
  112. const annotations = dash.annotations.list.filter((annotation) => annotation.enable);
  113. dash.annotations.list = annotations.map((annotation) => {
  114. return {
  115. name: annotation.name,
  116. enable: annotation.enable,
  117. iconColor: annotation.iconColor,
  118. snapshotData: annotation.snapshotData,
  119. type: annotation.type,
  120. builtIn: annotation.builtIn,
  121. hide: annotation.hide,
  122. };
  123. });
  124. // remove template queries
  125. dash.getVariables().forEach((variable: any) => {
  126. variable.query = '';
  127. variable.options = variable.current ? [variable.current] : [];
  128. variable.refresh = VariableRefresh.never;
  129. });
  130. // snapshot single panel
  131. if (panel) {
  132. const singlePanel = panel.getSaveModel();
  133. singlePanel.gridPos.w = 24;
  134. singlePanel.gridPos.x = 0;
  135. singlePanel.gridPos.y = 0;
  136. singlePanel.gridPos.h = 20;
  137. dash.panels = [singlePanel];
  138. }
  139. // cleanup snapshotData
  140. delete this.dashboard.snapshot;
  141. this.dashboard.forEachPanel((panel: PanelModel) => {
  142. delete panel.snapshotData;
  143. });
  144. this.dashboard.annotations.list.forEach((annotation) => {
  145. delete annotation.snapshotData;
  146. });
  147. };
  148. deleteSnapshot = async () => {
  149. const { deleteUrl } = this.state;
  150. await getBackendSrv().get(deleteUrl);
  151. this.setState({ step: 3 });
  152. };
  153. getSnapshotUrl = () => {
  154. return this.state.snapshotUrl;
  155. };
  156. onSnapshotNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  157. this.setState({ snapshotName: event.target.value });
  158. };
  159. onTimeoutChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  160. this.setState({ timeoutSeconds: Number(event.target.value) });
  161. };
  162. onExpireChange = (option: SelectableValue<number>) => {
  163. this.setState({
  164. selectedExpireOption: option,
  165. snapshotExpires: option.value,
  166. });
  167. };
  168. onSnapshotUrlCopy = () => {
  169. appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
  170. };
  171. renderStep1() {
  172. const { onDismiss } = this.props;
  173. const { snapshotName, selectedExpireOption, timeoutSeconds, isLoading, sharingButtonText, externalEnabled } =
  174. this.state;
  175. return (
  176. <>
  177. <div>
  178. <p className="share-modal-info-text">
  179. A snapshot is an instant way to share an interactive dashboard publicly. When created, we strip sensitive
  180. data like queries (metric, template, and annotation) and panel links, leaving only the visible metric data
  181. and series names embedded in your dashboard.
  182. </p>
  183. <p className="share-modal-info-text">
  184. Keep in mind, your snapshot <em>can be viewed by anyone</em> that has the link and can access the URL. Share
  185. wisely.
  186. </p>
  187. </div>
  188. <Field label="Snapshot name">
  189. <Input id="snapshot-name-input" width={30} value={snapshotName} onChange={this.onSnapshotNameChange} />
  190. </Field>
  191. <Field label="Expire">
  192. <Select
  193. inputId="expire-select-input"
  194. width={30}
  195. options={expireOptions}
  196. value={selectedExpireOption}
  197. onChange={this.onExpireChange}
  198. />
  199. </Field>
  200. <Field
  201. label="Timeout (seconds)"
  202. description="You might need to configure the timeout value if it takes a long time to collect your dashboard
  203. metrics."
  204. >
  205. <Input id="timeout-input" type="number" width={21} value={timeoutSeconds} onChange={this.onTimeoutChange} />
  206. </Field>
  207. <Modal.ButtonRow>
  208. <Button variant="secondary" onClick={onDismiss} fill="outline">
  209. Cancel
  210. </Button>
  211. {externalEnabled && (
  212. <Button variant="secondary" disabled={isLoading} onClick={this.createSnapshot(true)}>
  213. {sharingButtonText}
  214. </Button>
  215. )}
  216. <Button variant="primary" disabled={isLoading} onClick={this.createSnapshot()}>
  217. Local Snapshot
  218. </Button>
  219. </Modal.ButtonRow>
  220. </>
  221. );
  222. }
  223. renderStep2() {
  224. const { snapshotUrl } = this.state;
  225. return (
  226. <>
  227. <div className="gf-form" style={{ marginTop: '40px' }}>
  228. <div className="gf-form-row">
  229. <a href={snapshotUrl} className="large share-modal-link" target="_blank" rel="noreferrer">
  230. <Icon name="external-link-alt" /> {snapshotUrl}
  231. </a>
  232. <br />
  233. <ClipboardButton variant="secondary" getText={this.getSnapshotUrl} onClipboardCopy={this.onSnapshotUrlCopy}>
  234. Copy Link
  235. </ClipboardButton>
  236. </div>
  237. </div>
  238. <div className="pull-right" style={{ padding: '5px' }}>
  239. Did you make a mistake?{' '}
  240. <LinkButton fill="text" target="_blank" onClick={this.deleteSnapshot}>
  241. Delete snapshot.
  242. </LinkButton>
  243. </div>
  244. </>
  245. );
  246. }
  247. renderStep3() {
  248. return (
  249. <div className="share-modal-header">
  250. <p className="share-modal-info-text">
  251. The snapshot has been deleted. If you have already accessed it once, then it might take up to an hour before
  252. before it is removed from browser caches or CDN caches.
  253. </p>
  254. </div>
  255. );
  256. }
  257. render() {
  258. const { isLoading, step } = this.state;
  259. return (
  260. <>
  261. {step === 1 && this.renderStep1()}
  262. {step === 2 && this.renderStep2()}
  263. {step === 3 && this.renderStep3()}
  264. {isLoading && <Spinner inline={true} />}
  265. </>
  266. );
  267. }
  268. }