import {Fragment, useCallback, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {createDashboard} from 'sentry/actionCreators/dashboards'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {openModal} from 'sentry/actionCreators/modal'; import {Button} from 'sentry/components/button'; import TextArea from 'sentry/components/forms/controls/textarea'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import PanelTable from 'sentry/components/panels/panelTable'; import Tag from 'sentry/components/tag'; import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types'; import {convertToDashboardWidget} from 'sentry/utils/metrics/dashboard'; import type {ParseResult} from 'sentry/utils/metrics/dashboardImport'; import {parseDashboard} from 'sentry/utils/metrics/dashboardImport'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import { assignDefaultLayout, getInitialColumnDepths, } from 'sentry/views/dashboards/layoutUtils'; import {DDMContextProvider, useDDMContext} from 'sentry/views/ddm/context'; import {OrganizationContext} from 'sentry/views/organizationContext'; export function openDashboardImport(organization: Organization) { return openModal( deps => ( ), {modalCss} ); } type FormState = { dashboard: string; importResult: ParseResult | null; isValid: boolean; step: 'initial' | 'importing' | 'add-widgets'; }; function DashboardImportModal({Header, Body, Footer}: ModalRenderProps) { const api = useApi(); const router = useRouter(); const {metricsMeta} = useDDMContext(); const {selection} = usePageFilters(); const organization = useOrganization(); const [formState, setFormState] = useState({ step: 'initial', dashboard: '', importResult: null, isValid: false, }); const handleImportDashboard = useCallback(async () => { if (formState.isValid) { setFormState(curr => ({...curr, step: 'importing'})); const dashboardJson = JSON.parse(formState.dashboard); const importResult = await parseDashboard(dashboardJson, metricsMeta); setFormState(curr => ({ ...curr, importResult, step: 'add-widgets', })); } }, [formState.isValid, formState.dashboard, metricsMeta]); const handleCreateDashboard = useCallback(async () => { const title = formState.importResult?.title ?? 'Metrics Dashboard'; const importedWidgets = (formState.importResult?.widgets ?? []) .filter(widget => !!widget.mri) .map(widget => convertToDashboardWidget({...widget, ...selection}, widget.displayType) ) // Only import the first 30 widgets because of dashboard widget limit .slice(0, 30); const newDashboard = { id: 'temp-id-imported-dashboard', title: `${title} (Imported)`, description: formState.importResult?.description ?? '', filters: {}, dateCreated: '', widgets: assignDefaultLayout(importedWidgets, getInitialColumnDepths()), ...selection, }; const dashboard = await createDashboard(api, organization.slug, newDashboard); router.push( normalizeUrl({ pathname: `/organizations/${organization.slug}/dashboards/${dashboard.id}/`, }) ); }, [formState.importResult, selection, organization, api, router]); return (

{t('Import dashboard')}

{formState.step === 'initial' && ( { const isValid = isValidJson(e.target.value); setFormState(curr => ({...curr, dashboard: e.target.value, isValid})); }} /> )} {formState.step === 'importing' && } {formState.step === 'add-widgets' && (
{t( 'Processed %s widgets from the dashboard', formState.importResult?.report.length )}
{formState.importResult?.report.map(widget => { return (
{widget.title}
{widget.outcome}
{widget.errors.join(', ')}
{widget.notes.join(', ')}
); })}
{t( 'Found %s widgets that can be imported', formState.importResult?.widgets.length )}
)}
); } const ContentWrapper = styled('div')` display: grid; grid-template-columns: 1fr; gap: ${space(2)}; max-height: 70vh; overflow-y: scroll; `; const JSONTextArea = styled(TextArea)` min-height: 200px; `; const modalCss = css` width: 80%; `; const isValidJson = (str: string) => { try { JSON.parse(str); } catch (e) { return false; } return true; };