Browse Source

Import dashboard from json file (internal feature) (#52307)

Allows upload & import of dashboard via a JSON file.

- basic filetype validation
- preview of file contents before import
- feature flagged via https://flagr.getsentry.net/#/flags/392
---------

Co-authored-by: Nar Saynorath <nar.saynorath@gmail.com>
Chris Stavitsky 1 year ago
parent
commit
8b15332131

+ 10 - 0
static/app/actionCreators/modal.tsx

@@ -268,6 +268,16 @@ export async function openAddToDashboardModal(options) {
   });
 }
 
+export async function openImportDashboardFromFileModal(options) {
+  const mod = await import('sentry/components/modals/importDashboardFromFileModal');
+  const {default: Modal, modalCss} = mod;
+
+  openModal(deps => <Modal {...deps} {...options} />, {
+    closeEvents: 'escape-key',
+    modalCss,
+  });
+}
+
 export async function openReprocessEventModal({
   onClose,
   ...options

+ 127 - 0
static/app/components/modals/importDashboardFromFileModal.tsx

@@ -0,0 +1,127 @@
+import {Fragment, useState} from 'react';
+import {browserHistory} from 'react-router';
+import {css} from '@emotion/react';
+
+import {createDashboard} from 'sentry/actionCreators/dashboards';
+import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
+import {Button} from 'sentry/components/button';
+import {CodeSnippet} from 'sentry/components/codeSnippet';
+import {IconUpload} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import {
+  assignDefaultLayout,
+  getInitialColumnDepths,
+} from 'sentry/views/dashboards/layoutUtils';
+import {Wrapper} from 'sentry/views/discover/table/quickContext/styles';
+
+// Internal feature - no specs written.
+function ImportDashboardFromFileModal({
+  Header,
+  Body,
+  Footer,
+  organization,
+  api,
+  location,
+}) {
+  const [dashboardData, setDashboardData] = useState('');
+  const [validated, setValidated] = useState(false);
+
+  function validateFile(fileToUpload) {
+    if (!fileToUpload || !(fileToUpload.type === 'application/json')) {
+      addErrorMessage('You must upload a JSON file');
+      setValidated(false);
+      setDashboardData('');
+      return false;
+    }
+
+    setValidated(true);
+    return true;
+  }
+
+  const handleFileChange = e => {
+    const fileToUpload = e.target.files[0];
+    if (validateFile(fileToUpload)) {
+      const fileReader = new FileReader();
+      fileReader.readAsText(fileToUpload, 'UTF-8');
+      fileReader.onload = event => {
+        const target = event.target;
+        if (target && typeof target.result === 'string') {
+          const parsed = JSON.parse(target.result);
+          const formatted = JSON.stringify(parsed, null, '\t');
+          setDashboardData(formatted);
+        }
+      };
+    }
+  };
+
+  const handleUploadClick = async () => {
+    const dashboard = JSON.parse(dashboardData);
+
+    try {
+      const newDashboard = await createDashboard(
+        api,
+        organization.slug,
+        {
+          ...dashboard,
+          widgets: assignDefaultLayout(dashboard.widgets, getInitialColumnDepths()),
+        },
+        true
+      );
+
+      addSuccessMessage(`${dashboard.title} dashboard template successfully added`);
+      loadDashboard(newDashboard.id);
+    } catch (error) {
+      addErrorMessage('Could not upload dashboard, JSON may be invalid');
+    }
+  };
+
+  const loadDashboard = (dashboardId: string) => {
+    browserHistory.push(
+      normalizeUrl({
+        pathname: `/organizations/${organization.slug}/dashboards/${dashboardId}/`,
+        query: location.query,
+      })
+    );
+  };
+
+  return (
+    <Fragment>
+      <Header closeButton>
+        <h4>{t('Import Dashboard from JSON File')}</h4>
+      </Header>
+      <Body>
+        <Wrapper>
+          <input type="file" onChange={handleFileChange} />
+        </Wrapper>
+        <Wrapper>
+          <Button
+            onClick={handleUploadClick}
+            size="md"
+            disabled={!validated}
+            priority="primary"
+            icon={<IconUpload />}
+          >
+            {t('Import')}
+          </Button>
+        </Wrapper>
+      </Body>
+      {validated && (
+        <Fragment>
+          <Footer />
+          <Wrapper>
+            <h4>{t('Preview')}</h4>
+          </Wrapper>
+          <CodeSnippet language="json">{dashboardData}</CodeSnippet>
+        </Fragment>
+      )}
+    </Fragment>
+  );
+}
+
+export default ImportDashboardFromFileModal;
+
+export const modalCss = css`
+  max-width: 700px;
+  margin: 70px auto;
+`;

+ 18 - 2
static/app/views/dashboards/manage/index.tsx

@@ -4,6 +4,7 @@ import pick from 'lodash/pick';
 
 import {createDashboard} from 'sentry/actionCreators/dashboards';
 import {addSuccessMessage} from 'sentry/actionCreators/indicator';
+import {openImportDashboardFromFileModal} from 'sentry/actionCreators/modal';
 import {Client} from 'sentry/api';
 import Feature from 'sentry/components/acl/feature';
 import {Alert} from 'sentry/components/alert';
@@ -161,7 +162,6 @@ class ManageDashboards extends AsyncView<Props, State> {
 
   renderActions() {
     const activeSort = this.getActiveSort();
-
     return (
       <StyledActions>
         <SearchBar
@@ -279,7 +279,7 @@ class ManageDashboards extends AsyncView<Props, State> {
 
   renderBody() {
     const {showTemplates} = this.state;
-    const {organization} = this.props;
+    const {organization, api, location} = this.props;
 
     return (
       <Feature
@@ -324,6 +324,22 @@ class ManageDashboards extends AsyncView<Props, State> {
                     >
                       {t('Create Dashboard')}
                     </Button>
+                    <Feature features={['dashboards-import']}>
+                      <Button
+                        onClick={() => {
+                          openImportDashboardFromFileModal({
+                            organization,
+                            api,
+                            location,
+                          });
+                        }}
+                        size="sm"
+                        priority="primary"
+                        icon={<IconAdd isCircled />}
+                      >
+                        {t('Import Dashboard from JSON')}
+                      </Button>
+                    </Feature>
                   </ButtonBar>
                 </Layout.HeaderActions>
               </Layout.Header>