266 lines
7.2 KiB
TypeScript

import './importJSON.styles.scss';
import { red } from '@ant-design/colors';
import { ExclamationCircleTwoTone } from '@ant-design/icons';
import MEditor, { Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import {
Button,
Flex,
Modal,
Space,
Typography,
Upload,
UploadProps,
} from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
import { useErrorModal } from 'providers/ErrorModalProvider';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { DashboardData } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
function ImportJSON({
isImportJSONModalVisible,
uploadedGrafana,
onModalHandler,
}: ImportJSONProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [jsonData, setJsonData] = useState<Record<string, unknown>>();
const { t } = useTranslation(['dashboard', 'common']);
const [isUploadJSONError, setIsUploadJSONError] = useState<boolean>(false);
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
false,
);
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
const [editorValue, setEditorValue] = useState<string>('');
const { notifications } = useNotifications();
const onChangeHandler: UploadProps['onChange'] = (info) => {
const { fileList } = info;
const reader = new FileReader();
const lastFile = fileList[fileList.length - 1];
if (lastFile.originFileObj) {
reader.onload = async (event): Promise<void> => {
if (event.target) {
const target = event.target.result;
try {
if (target) {
const targetFile = target.toString();
const parsedValue = JSON.parse(targetFile);
setJsonData(parsedValue);
setEditorValue(JSON.stringify(parsedValue, null, 2));
setIsUploadJSONError(false);
}
} catch (error) {
setIsUploadJSONError(true);
}
}
};
reader.readAsText(lastFile.originFileObj);
}
};
const { showErrorModal } = useErrorModal();
const onClickLoadJsonHandler = async (): Promise<void> => {
try {
setDashboardCreating(true);
logEvent('Dashboard List: Import and next clicked', {});
const dashboardData = JSON.parse(editorValue) as DashboardData;
if (dashboardData?.layout) {
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
} else {
dashboardData.layout = [];
}
const response = await createDashboard({
...dashboardData,
uploadedGrafana,
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.data?.id,
dashboardName: response.data?.data?.title,
});
setDashboardCreating(false);
} catch (error) {
showErrorModal(error as APIError);
setDashboardCreating(false);
setIsCreateDashboardError(true);
notifications.error({
message: error instanceof Error ? error.message : t('error_loading_json'),
});
}
};
const getErrorNode = (error: string): JSX.Element => (
<Space>
<ExclamationCircleTwoTone twoToneColor={[red[7], '#1f1f1f']} />
<Typography style={{ color: '#D89614' }}>{error}</Typography>
</Space>
);
const onCancelHandler = (): void => {
setIsUploadJSONError(false);
setIsCreateDashboardError(false);
onModalHandler();
};
const isDarkMode = useIsDarkMode();
function setEditorTheme(monaco: Monaco): void {
monaco.editor.defineTheme('my-theme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: {
'editor.background': Color.BG_INK_300,
},
});
}
return (
<Modal
wrapClassName="import-json-modal"
open={isImportJSONModalVisible}
centered
closable={false}
destroyOnClose
width="60vw"
footer={
<div className="import-json-modal-footer">
{isCreateDashboardError && (
<div className="create-dashboard-json-error">
{getErrorNode(t('error_loading_json'))}
</div>
)}
{isUploadJSONError && (
<div className="create-dashboard-json-error">
{getErrorNode(t('error_upload_json'))}
</div>
)}
<div className="action-btns-container">
<Flex gap="small">
<Upload
accept=".json"
showUploadList={false}
multiple={false}
onChange={onChangeHandler}
beforeUpload={(): boolean => false}
action="none"
data={jsonData}
>
<Button
type="default"
className="periscope-btn"
icon={<MonitorDot size={14} />}
onClick={(): void => {
logEvent('Dashboard List: Upload JSON file clicked', {});
}}
>
{' '}
{t('upload_json_file')}
</Button>
</Upload>
<a
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
target="_blank"
rel="noopener noreferrer"
>
<Button
type="default"
className="periscope-btn"
icon={<Github size={14} />}
>
{t('view_template')}&nbsp;
<ExternalLink size={14} />
</Button>
</a>
</Flex>
<Button
// disabled={editorValue.length === 0}
onClick={onClickLoadJsonHandler}
loading={dashboardCreating}
className="periscope-btn primary"
type="primary"
>
{t('import_and_next')} &nbsp; <MoveRight size={14} />
</Button>
</div>
</div>
}
>
<div className="import-json-content-container">
<div className="import-json-content-header">
<Typography.Text>{t('import_json')}</Typography.Text>
<X size={14} className="periscope-btn ghost" onClick={onCancelHandler} />
</div>
<MEditor
language="json"
height="40vh"
onChange={(newValue): void => setEditorValue(newValue || '')}
value={editorValue}
options={{
scrollbar: {
alwaysConsumeMouseWheel: false,
},
minimap: {
enabled: false,
},
fontSize: 14,
fontFamily: 'Space Mono',
}}
theme={isDarkMode ? 'my-theme' : 'light'}
onMount={(_, monaco): void => {
document.fonts.ready.then(() => {
monaco.editor.remeasureFonts();
});
}}
// eslint-disable-next-line react/jsx-no-bind
beforeMount={setEditorTheme}
/>
</div>
</Modal>
);
}
interface ImportJSONProps {
isImportJSONModalVisible: boolean;
onModalHandler: VoidFunction;
uploadedGrafana: boolean;
}
export default ImportJSON;