UNPKG

datajunction-ui

Version:
330 lines (315 loc) 9.95 kB
import * as React from 'react'; import { useParams } from 'react-router-dom'; import { useContext, useEffect, useState } from 'react'; import Tab from '../../components/Tab'; import NamespaceHeader from '../../components/NamespaceHeader'; import NodeInfoTab from './NodeInfoTab'; import NodeColumnTab from './NodeColumnTab'; import NodeDataFlowTab from './NodeDataFlowTab'; import NodeDimensionsTab from './NodeDimensionsTab'; import NodeHistory from './NodeHistory'; import NotebookDownload from './NotebookDownload'; import DJClientContext from '../../providers/djclient'; import NodeMaterializationTab from './NodeMaterializationTab'; import NodePreAggregationsTab from './NodePreAggregationsTab'; import ClientCodePopover from './ClientCodePopover'; import WatchButton from './WatchNodeButton'; import NodesWithDimension from './NodesWithDimension'; import NodeColumnLineage from './NodeLineageTab'; import EditIcon from '../../icons/EditIcon'; import ChartIcon from '../../icons/ChartIcon'; import AlertIcon from '../../icons/AlertIcon'; import LoadingIcon from '../../icons/LoadingIcon'; import NodeDependenciesTab from './NodeDependenciesTab'; import { useNavigate } from 'react-router-dom'; export function NodePage() { const djClient = useContext(DJClientContext).DataJunctionAPI; const navigate = useNavigate(); const { name, tab } = useParams(); const [state, setState] = useState({ selectedTab: tab || 'info', }); const [node, setNode] = useState(null); // undefined = not yet known; NamespaceHeader reports the read-only verdict // (git_only, flat/root shape, or git-deployed) once its config + sources load. const [isReadOnly, setIsReadOnly] = useState(undefined); const onClickTab = id => () => { // Preview tab redirects to Query Planner instead of showing content if (id === 'preview') { const param = node?.type === 'cube' ? 'cube' : 'metrics'; navigate(`/planner?${param}=${encodeURIComponent(name)}`); return; } navigate(`/nodes/${name}/${id}`); setState({ selectedTab: id }); }; const buildTabs = tab => { return tab.display ? ( <Tab key={tab.id} id={tab.id} name={tab.name} icon={tab.icon} onClick={onClickTab(tab.id)} selectedTab={state.selectedTab} /> ) : null; }; useEffect(() => { const fetchData = async () => { const data = await djClient.node(name); if (data.message !== undefined) { // Error response setNode(data); return; } setNode({ ...data }); }; fetchData().catch(console.error); }, [djClient, name]); const tabsList = node => { return [ { id: 'info', name: 'Info', display: true, }, { id: 'columns', name: 'Columns', display: node?.type !== 'metric', }, { id: 'data-flow', name: 'Data Flow', display: true, }, { id: 'dimensions', name: 'Dimensions', display: node?.type !== 'source', }, { id: 'history', name: 'History', display: true, }, { id: 'materializations', name: 'Materializations', display: node?.type !== 'source' && node?.type !== 'metric', }, { id: 'linked', name: 'Linked Nodes', display: node?.type === 'dimension', }, { id: 'lineage', name: 'Lineage', display: node?.type === 'metric', }, { id: 'dependencies', name: 'Dependencies', display: node?.type !== 'cube', }, { id: 'preview', name: 'Preview →', icon: <ChartIcon size={18} />, display: node?.type === 'metric' || node?.type === 'cube', }, ]; }; let tabToDisplay = null; switch (state.selectedTab) { case 'info': tabToDisplay = node ? <NodeInfoTab node={node} /> : ''; break; case 'columns': tabToDisplay = ( <NodeColumnTab node={node} djClient={djClient} readOnly={!!isReadOnly} /> ); break; case 'data-flow': tabToDisplay = <NodeDataFlowTab djNode={node} />; break; case 'dimensions': tabToDisplay = <NodeDimensionsTab djNode={node} />; break; case 'history': tabToDisplay = <NodeHistory node={node} djClient={djClient} />; break; case 'materializations': // Cube nodes use cube-specific materialization tab // Other nodes (transform, metric, dimension) use pre-aggregations tab tabToDisplay = node?.type === 'cube' ? ( <NodeMaterializationTab node={node} djClient={djClient} /> ) : ( <NodePreAggregationsTab node={node} /> ); break; case 'linked': tabToDisplay = <NodesWithDimension node={node} djClient={djClient} />; break; case 'lineage': tabToDisplay = <NodeColumnLineage djNode={node} djClient={djClient} />; break; case 'dependencies': tabToDisplay = <NodeDependenciesTab node={node} djClient={djClient} />; break; default: /* istanbul ignore next */ tabToDisplay = <NodeInfoTab node={node} />; } const buttonStyle = { height: '28px', padding: '0 10px', fontSize: '12px', border: '1px solid #e2e8f0', borderRadius: '4px', backgroundColor: '#ffffff', color: '#475569', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px', whiteSpace: 'nowrap', }; const NodeButtons = () => { return ( <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> {isReadOnly === false && ( <button style={buttonStyle} onClick={() => navigate(`/nodes/${node?.name}/edit`)} > <EditIcon /> Edit </button> )} <WatchButton node={node} buttonStyle={buttonStyle} /> <ClientCodePopover nodeName={name} buttonStyle={buttonStyle} /> {node?.type === 'cube' && ( <NotebookDownload node={node} buttonStyle={buttonStyle} /> )} </div> ); }; // @ts-ignore return ( <div className="node__header"> <NamespaceHeader namespace={name.split('.').slice(0, -1).join('.')} onReadOnlyChange={setIsReadOnly} /> <div className="card"> {node === undefined ? ( <div style={{ padding: '2rem', textAlign: 'center' }}> <LoadingIcon /> </div> ) : node?.message === undefined ? ( <div className="card-header" style={{}}> <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', }} > <h3 className="card-title align-items-start flex-column" style={{ display: 'inline-block' }} > <span className="card-label fw-bold text-gray-800" role="dialog" aria-hidden="false" aria-label="DisplayName" > {node?.display_name}{' '} <span className={'node_type__' + node?.type + ' badge node_type'} role="dialog" aria-hidden="false" aria-label="NodeType" > {node?.type} </span> {isReadOnly === true && ( <span style={{ display: 'inline-flex', alignItems: 'center', marginLeft: '4px', color: '#92400e', verticalAlign: 'middle', cursor: 'help', }} aria-label="ReadOnly" title="Read-only — edits must go through git." > <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" > <rect x="3" y="11" width="18" height="11" rx="2" ry="2" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" /> </svg> </span> )} </span> </h3> <NodeButtons /> </div> <div> <a href={'/nodes/' + node?.name} className="link-table" role="dialog" aria-hidden="false" aria-label="NodeName" > {node?.name} </a> <span className="rounded-pill badge bg-secondary-soft" style={{ marginLeft: '0.5rem' }} > {node?.version} </span> </div> <div className="dj-tabs-bar">{tabsList(node).map(buildTabs)}</div> {tabToDisplay} </div> ) : node?.message !== undefined ? ( <div className="message alert" style={{ margin: '20px' }}> <AlertIcon /> Node `{name}` does not exist! </div> ) : ( '' )} </div> </div> ); }