datajunction-ui
Version:
DataJunction UI
330 lines (315 loc) • 9.95 kB
JSX
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>
);
}