import {Documentation, Properties, SearchItem, SearchResult} from '~/resource/graphs/types'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {backgroundColor, borderColor, contentHeight, position, primaryColor, rem, size} from '~/utils/style'; import ChartToolbox from '~/components/ChartToolbox'; import HashLoader from 'react-spinners/HashLoader'; import styled from 'styled-components'; import {toast} from 'react-toastify'; import {useTranslation} from '~/utils/i18n'; const PUBLIC_PATH = process.env.PUBLIC_PATH; const toolboxHeight = rem(40); const Wrapper = styled.div` position: relative; height: ${contentHeight}; background-color: ${backgroundColor}; display: flex; flex-direction: column; justify-content: center; align-items: center; `; const RenderContent = styled.div<{show: boolean}>` position: absolute; top: 0; left: 0; ${size('100%', '100%')} opacity: ${props => (props.show ? 1 : 0)}; z-index: ${props => (props.show ? 0 : -1)}; pointer-events: ${props => (props.show ? 'auto' : 'none')}; `; const Toolbox = styled(ChartToolbox)` height: ${toolboxHeight}; border-bottom: 1px solid ${borderColor}; padding: 0 ${rem(20)}; `; const Content = styled.div` position: relative; height: calc(100% - ${toolboxHeight}); > iframe { ${size('100%', '100%')} border: none; } > .powered-by { display: block; ${position('absolute', null, null, rem(20), rem(30))} color: #ddd; font-size: ${rem(14)}; user-select: none; img { height: 1em; opacity: 0.5; vertical-align: middle; } } `; const Loading = styled.div` ${size('100%', '100%')} display: flex; flex-direction: column; justify-content: center; align-items: center; overscroll-behavior: none; cursor: progress; font-size: ${rem(16)}; line-height: ${rem(60)}; `; export type GraphRef = { export(type: 'svg' | 'png'): void; search(value: string): void; select(item: SearchItem): void; showModelProperties(): void; showNodeDocumentation: (data: Properties) => void; }; type GraphProps = { files: FileList | File[] | null; uploader: JSX.Element; showAttributes: boolean; showInitializers: boolean; showNames: boolean; onRendered?: () => unknown; onSearch?: (data: SearchResult) => unknown; onShowModelProperties?: (data: Properties) => unknown; onShowNodeProperties?: (data: Properties) => unknown; onShowNodeDocumentation?: (data: Documentation) => unknown; }; const Graph = React.forwardRef( ( { files, uploader, showAttributes, showInitializers, showNames, onRendered, onSearch, onShowModelProperties, onShowNodeProperties, onShowNodeDocumentation }, ref ) => { const {t} = useTranslation('graphs'); const [ready, setReady] = useState(false); const [loading, setLoading] = useState(false); const [rendered, setRendered] = useState(false); const iframe = useRef(null); const handler = useCallback( (event: MessageEvent) => { if (event.data) { const {type, data} = event.data; switch (type) { case 'status': switch (data) { case 'ready': return setReady(true); case 'loading': return setLoading(true); case 'rendered': setLoading(false); setRendered(true); onRendered?.(); return; } return; case 'search': return onSearch?.(data); case 'cancel': return setLoading(false); case 'error': toast(data); setLoading(false); return; case 'show-model-properties': return onShowModelProperties?.(data); case 'show-node-properties': return onShowNodeProperties?.(data); case 'show-node-documentation': return onShowNodeDocumentation?.(data); } } }, [onRendered, onSearch, onShowModelProperties, onShowNodeProperties, onShowNodeDocumentation] ); const dispatch = useCallback((type: string, data?: unknown) => { if (process.browser) { iframe.current?.contentWindow?.postMessage( { type, data }, `${window.location.protocol}//${window.location.host}` ); } }, []); useEffect(() => { if (process.browser) { window.addEventListener('message', handler); dispatch('ready'); return () => { window.removeEventListener('message', handler); }; } }, [handler, dispatch]); useEffect(() => (ready && dispatch('change-files', files)) || undefined, [dispatch, files, ready]); useEffect(() => (ready && dispatch('toggle-attributes', showAttributes)) || undefined, [ dispatch, showAttributes, ready ]); useEffect(() => (ready && dispatch('toggle-initializers', showInitializers)) || undefined, [ dispatch, showInitializers, ready ]); useEffect(() => (ready && dispatch('toggle-names', showNames)) || undefined, [dispatch, showNames, ready]); useImperativeHandle(ref, () => ({ export(type) { dispatch('export', type); }, search(value) { dispatch('search', value); }, select(item) { dispatch('select', item); }, showModelProperties() { dispatch('show-model-properties'); }, showNodeDocumentation(data) { dispatch('show-node-documentation', data); } })); const content = useMemo(() => { if (!ready || loading) { return ( ); } if (ready && !rendered) { return uploader; } return null; }, [ready, loading, rendered, uploader]); return ( {content} dispatch('zoom-reset') }, { icon: 'zoom-out', tooltip: t('graphs:zoom-out'), onClick: () => dispatch('zoom-out') }, { icon: 'zoom-in', tooltip: t('graphs:zoom-in'), onClick: () => dispatch('zoom-in') } ]} reversed tooltipPlacement="bottom" /> Powered by netron ); } ); Graph.displayName = 'Graph'; export default Graph;