123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- 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 type {ParseResult} from 'sentry/utils/metrics/dashboardImport';
- import {parseDashboard} from 'sentry/utils/metrics/dashboardImport';
- import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
- 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 {OrganizationContext} from 'sentry/views/organizationContext';
- export function openDashboardImport(organization: Organization) {
- return openModal(
- deps => (
- <OrganizationContext.Provider value={organization}>
- <DashboardImportModal {...deps} />
- </OrganizationContext.Provider>
- ),
- {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 {selection} = usePageFilters();
- // we want to get all custom metrics for organization
- const {data: metricsMeta} = useMetricsMeta({projects: [-1]}, ['custom']);
- const organization = useOrganization();
- const [formState, setFormState] = useState<FormState>({
- 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 ?? [])
- // 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 (
- <Fragment>
- <Header>
- <h4>{t('Import dashboard')}</h4>
- </Header>
- <Body>
- <ContentWrapper>
- {formState.step === 'initial' && (
- <JSONTextArea
- rows={4}
- maxRows={20}
- name="dashboard"
- placeholder={t('Paste dashboard JSON ')}
- value={formState.dashboard}
- onChange={e => {
- const isValid = isValidJson(e.target.value);
- setFormState(curr => ({...curr, dashboard: e.target.value, isValid}));
- }}
- />
- )}
- {formState.step === 'importing' && <LoadingIndicator />}
- {formState.step === 'add-widgets' && (
- <Fragment>
- <div>
- {t(
- 'Processed %s widgets from the dashboard',
- formState.importResult?.report.length
- )}
- </div>
- <PanelTable headers={['Title', 'Outcome', 'Errors']}>
- {formState.importResult?.report.map(widget => {
- return (
- <Fragment key={widget.id}>
- <div>{widget.title}</div>
- <div>
- <Tag type={widget.outcome}>{widget.outcome}</Tag>
- </div>
- <div>{widget.errors.join(', ')}</div>
- </Fragment>
- );
- })}
- </PanelTable>
- <div>
- {t(
- 'Found %s widgets that can be imported',
- formState.importResult?.widgets.length
- )}
- </div>
- </Fragment>
- )}
- </ContentWrapper>
- </Body>
- <Footer>
- <Tooltip
- disabled={formState.isValid}
- title={t('Please input valid dashboard JSON')}
- >
- <Button
- priority="primary"
- disabled={!formState.isValid}
- onClick={
- formState.step === 'initial' ? handleImportDashboard : handleCreateDashboard
- }
- >
- {formState.step === 'initial' ? t('Import') : t('Create Dashboard')}
- </Button>
- </Tooltip>
- </Footer>
- </Fragment>
- );
- }
- 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;
- };
|