exportDashboard.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import cloneDeep from 'lodash/cloneDeep';
  2. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  3. import type {DashboardDetails} from './types';
  4. type ExcludedProperties = 'createdBy' | 'dateCreated' | 'id' | 'dashboardId' | 'widgetId';
  5. async function exportDashboard() {
  6. try {
  7. const structure = {
  8. base_url: null,
  9. dashboard_id: null,
  10. org_slug: null,
  11. };
  12. const params = getAPIParams(structure);
  13. const apiUrl = `https://${params.base_url}/api/0/organizations/${params.org_slug}/dashboards/${params.dashboard_id}/`;
  14. const response = await fetch(apiUrl);
  15. const jsonData = await response.json();
  16. const normalized = normalizeData(jsonData);
  17. normalized.projects = [];
  18. downloadObjectAsJson(normalized, cleanTitle(normalized.title));
  19. } catch (error) {
  20. addErrorMessage(
  21. 'Could not export dashboard. Please wait or try again with a different dashboard'
  22. );
  23. }
  24. }
  25. function getAPIParams(structure: any) {
  26. const url = window.location.href;
  27. const regex = {
  28. base_url: /(\/\/)(.*?)(\/)/,
  29. dashboard_id: /(dashboard\/)(.*?)(\/)/,
  30. org_slug: /(\/\/)(.+?)(?=\.)/,
  31. };
  32. for (const attr in regex) {
  33. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  34. const match = url.match(regex[attr]);
  35. if (match?.length) {
  36. structure[attr] = match.length >= 3 ? match[2] : null;
  37. }
  38. }
  39. return structure;
  40. }
  41. function normalizeData(
  42. source: DashboardDetails
  43. ): Omit<DashboardDetails, ExcludedProperties> {
  44. const payload: Omit<DashboardDetails, ExcludedProperties> = {
  45. title: '',
  46. filters: {},
  47. projects: [],
  48. widgets: [],
  49. environment: [],
  50. };
  51. for (const property in payload) {
  52. if (property in source) {
  53. let data: any[] = [];
  54. // if there is a nested object with properties that should be deleted
  55. if (['widgets'].includes(property)) {
  56. // get the object properties so that we can loop through them
  57. const type = getPropertyStructure(property);
  58. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  59. data = normalizeNestedObject(source[property], type);
  60. } else {
  61. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  62. data = source[property];
  63. }
  64. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  65. payload[property] = data;
  66. }
  67. }
  68. return payload;
  69. }
  70. function normalizeNestedObject(object: any, structure: any) {
  71. const nestedObjectArray: any[] = [];
  72. for (const index in object) {
  73. const nestedObject = cloneDeep(structure);
  74. for (const property in structure) {
  75. if (property in object[index]) {
  76. let data: any[] = [];
  77. if (['queries'].includes(property)) {
  78. // get the object properties so that we can loop through them
  79. const type = getPropertyStructure(property);
  80. data = normalizeNestedObject(object[index][property], type);
  81. } else {
  82. data = object[index][property];
  83. }
  84. nestedObject[property] = data;
  85. }
  86. }
  87. nestedObjectArray.push(nestedObject);
  88. }
  89. return nestedObjectArray;
  90. }
  91. function getPropertyStructure(property: any) {
  92. let structure = {};
  93. switch (property) {
  94. case 'widgets':
  95. structure = {
  96. title: '',
  97. description: '',
  98. interval: '',
  99. queries: [],
  100. displayType: '',
  101. widgetType: '',
  102. layout: [],
  103. };
  104. break;
  105. case 'queries':
  106. structure = {
  107. aggregates: [],
  108. columns: [],
  109. conditions: [],
  110. name: '',
  111. orderby: '',
  112. fieldAliases: [],
  113. fields: [],
  114. };
  115. break;
  116. default:
  117. structure = {};
  118. }
  119. return structure;
  120. }
  121. function downloadObjectAsJson(exportObj: any, exportName: any) {
  122. const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
  123. JSON.stringify(exportObj)
  124. )}`;
  125. const downloadAnchorNode = document.createElement('a');
  126. downloadAnchorNode.setAttribute('href', dataStr);
  127. downloadAnchorNode.setAttribute('download', `${exportName}.json`);
  128. document.body.appendChild(downloadAnchorNode); // required for firefox
  129. downloadAnchorNode.click();
  130. downloadAnchorNode.remove();
  131. }
  132. function cleanTitle(title: any) {
  133. const regex = /[^a-z0-9]/gi;
  134. const formattedTitle = title.replace(regex, '-');
  135. const date = new Date();
  136. return `${formattedTitle}-${date.toISOString()}`;
  137. }
  138. export default exportDashboard;