UNPKG

datajunction-ui

Version:
492 lines (482 loc) 16.8 kB
import { useEffect, useState } from 'react'; import * as React from 'react'; import EditColumnPopover from './EditColumnPopover'; import EditColumnDescriptionPopover from './EditColumnDescriptionPopover'; import ManageDimensionLinksDialog from './ManageDimensionLinksDialog'; import AddComplexDimensionLinkPopover from './AddComplexDimensionLinkPopover'; import { labelize } from '../../../utils/form'; import PartitionColumnPopover from './PartitionColumnPopover'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import foundation from 'react-syntax-highlighter/dist/esm/styles/hljs/foundation'; export default function NodeColumnTab({ node, djClient, readOnly = false }) { const [attributes, setAttributes] = useState([]); const [dimensions, setDimensions] = useState([]); const [columns, setColumns] = useState([]); const [links, setLinks] = useState([]); useEffect(() => { const fetchData = async () => { if (node) { setColumns(await djClient.columns(node)); } }; fetchData().catch(console.error); }, [djClient, node]); useEffect(() => { const fetchData = async () => { const attributes = await djClient.attributes(); const options = attributes.map(attr => { return { value: attr.name, label: labelize(attr.name) }; }); setAttributes(options); }; fetchData().catch(console.error); }, [djClient]); useEffect(() => { const fetchData = async () => { const dimensions = await djClient.dimensions(); const options = dimensions.map(dim => { return { value: dim.name, label: `${dim.name} (${dim.indegree} links)`, }; }); setDimensions(options); }; fetchData().catch(console.error); }, [djClient]); const showColumnAttributes = col => { return col.attributes.map((attr, idx) => ( <span className="node_type__dimension badge node_type" key={`col-attr-${col.name}-${idx}`} > {attr.attribute_type.name.replace(/_/, ' ')} </span> )); }; const showColumnPartition = col => { if (!col.partition) return ''; const isTemporal = col.partition.type_ === 'temporal'; const typeLabel = col.partition.type_.charAt(0).toUpperCase() + col.partition.type_.slice(1); const details = []; if (isTemporal) { if (col.partition.granularity) details.push(col.partition.granularity); if (col.partition.format) details.push(col.partition.format); } const icon = isTemporal ? ( <svg className="partition-cell__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" > <rect x="3" y="4" width="18" height="18" rx="2" ry="2" /> <line x1="16" y1="2" x2="16" y2="6" /> <line x1="8" y1="2" x2="8" y2="6" /> <line x1="3" y1="10" x2="21" y2="10" /> </svg> ) : ( <svg className="partition-cell__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" > <line x1="8" y1="6" x2="21" y2="6" /> <line x1="8" y1="12" x2="21" y2="12" /> <line x1="8" y1="18" x2="21" y2="18" /> <line x1="3" y1="6" x2="3.01" y2="6" /> <line x1="3" y1="12" x2="3.01" y2="12" /> <line x1="3" y1="18" x2="3.01" y2="18" /> </svg> ); return ( <div className="partition-cell"> {icon} <span className="partition-cell__type">{typeLabel}</span> {details.length > 0 ? ( <> <span className="partition-cell__dash"></span> <span className="partition-cell__details"> {details.map((d, i) => ( <React.Fragment key={i}> {i > 0 ? ', ' : ''} <code>{d}</code> </React.Fragment> ))} </span> </> ) : null} </div> ); }; const columnList = columns => { return columns?.map(col => { // FK Links: Only show links that specifically reference THIS column's foreign keys // Filter out complex dimension links that may join on multiple columns const fkLinksForColumn = ( links.length > 0 ? links : node?.dimension_links ) .filter(link => { // Check if this link has a foreign key entry for this specific column const foreignKeys = Object.keys(link.foreign_keys || {}); const columnKey = `${node.name}.${col.name}`; return foreignKeys.includes(columnKey); }) .map(link => link.dimension.name); // Check if this column has a reference dimension link const referenceLink = col.dimension ? { dimension: col.dimension.name, dimension_column: col.dimension_column, role: col.dimension.role, } : null; return ( <tr key={col.name} className="column-row"> <td className="text-start" role="columnheader" aria-label="ColumnName" aria-hidden="false" > {col.name} </td> <td> <span className="" role="columnheader" aria-label="ColumnDisplayName" aria-hidden="false" > {col.display_name} </span> </td> <td> <span className="" role="columnheader" aria-label="ColumnDescription" aria-hidden="false" > {col.description || ''} {!readOnly && ( <EditColumnDescriptionPopover column={col} node={node} onSubmit={async () => { const res = await djClient.node(node.name); setColumns(res.columns); }} /> )} </span> </td> <td> <span className={`node_type__${ node.type === 'cube' ? col.type : 'transform' } badge node_type`} role="columnheader" aria-label="ColumnType" aria-hidden="false" > {col.type} </span> </td> {node.type !== 'cube' ? ( <td className="dimension-links-cell"> <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '0.25rem', position: 'relative', }} > {fkLinksForColumn.length > 0 && ( <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', }} > {fkLinksForColumn.map(dimName => ( <span className="rounded-pill badge bg-secondary-soft dimension-badge" style={{ fontSize: '14px', position: 'relative' }} key={dimName} title="FK Link (via primary key)" > <a href={`/nodes/${dimName}`}>{dimName}</a> </span> ))} </div> )} {referenceLink && ( <span className="rounded-pill badge bg-info dimension-badge" style={{ fontSize: '14px', position: 'relative' }} title={`Reference Link: ${referenceLink.dimension}.${referenceLink.dimension_column}`} > <a href={`/nodes/${referenceLink.dimension}`}> {referenceLink.dimension}.{referenceLink.dimension_column} </a> </span> )} {!readOnly && ( <ManageDimensionLinksDialog column={col} node={node} dimensions={dimensions} fkLinks={fkLinksForColumn} referenceLink={referenceLink} onSubmit={async () => { const res = await djClient.node(node.name); setLinks(res.dimension_links); setColumns(res.columns); }} /> )} </div> </td> ) : ( '' )} {node.type !== 'cube' ? ( <td> {showColumnAttributes(col)} {!readOnly && ( <EditColumnPopover column={col} node={node} options={attributes} onSubmit={async () => { const res = await djClient.node(node.name); setColumns(res.columns); }} /> )} </td> ) : ( '' )} <td> {showColumnPartition(col)} {!readOnly && ( <PartitionColumnPopover column={col} node={node} onSubmit={async () => { const res = await djClient.node(node.name); setColumns(res.columns); }} /> )} </td> </tr> ); }); }; return ( <> <style> {` .dimension-link-edit:hover { opacity: 1 !important; color: #007bff !important; } .dimension-badge a { max-width: 300px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; direction: rtl; text-align: left; } `} </style> <div className="table-responsive"> <table className="card-inner-table table"> <thead className="fs-7 fw-bold text-gray-400 border-bottom-0"> <tr> <th className="text-start">Column</th> <th>Display Name</th> <th>Description</th> <th>Type</th> {node?.type !== 'cube' ? ( <> <th>Dimension Links</th> <th>Attributes</th> </> ) : ( '' )} <th>Partition</th> </tr> </thead> <tbody>{columnList(columns)}</tbody> </table> </div> <div> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1rem', }} > <h3 style={{ margin: 0 }}> Complex Dimension Links (Custom Join SQL) </h3> <AddComplexDimensionLinkPopover node={node} dimensions={dimensions} onSubmit={async () => { const res = await djClient.node(node.name); setLinks(res.dimension_links); }} /> </div> <table className="card-inner-table table"> <thead className="fs-7 fw-bold text-gray-400 border-bottom-0"> <tr> <th className="text-start">Dimension Node</th> <th>Join Type</th> <th>Join SQL</th> <th>Join Cardinality</th> <th>Role</th> <th>Actions</th> </tr> </thead> <tbody> {node?.dimension_links && node.dimension_links.length > 0 ? ( node.dimension_links.map((link, idx) => { return ( <tr key={`${link.dimension.name}-${link.role || idx}`}> <td> <a href={'/nodes/' + link.dimension.name}> {link.dimension.name} </a> </td> <td>{link.join_type.toUpperCase()}</td> <td style={{ maxWidth: '400px' }}> <SyntaxHighlighter language="sql" style={foundation} wrapLongLines={true} > {link.join_sql?.trim()} </SyntaxHighlighter> </td> <td> {link.join_cardinality ? link.join_cardinality.replace(/_/g, ' ').toUpperCase() : 'N/A'} </td> <td>{link.role || '-'}</td> <td> <div style={{ display: 'flex', gap: '0.5rem' }}> <AddComplexDimensionLinkPopover node={node} dimensions={dimensions} existingLink={link} isEditMode={true} onSubmit={async () => { const res = await djClient.node(node.name); setLinks(res.dimension_links); }} /> <button onClick={async () => { if ( window.confirm( `Remove link to ${link.dimension.name}?`, ) ) { try { const response = await djClient.removeComplexDimensionLink( node.name, link.dimension.name, link.role || null, ); if ( response.status === 200 || response.status === 201 || response.status === 204 ) { alert( 'Complex dimension link removed successfully!', ); window.location.reload(); } else { console.error('Remove link error:', response); alert( response.json?.message || `Failed to remove link (status: ${response.status})`, ); } } catch (error) { console.error('Remove link exception:', error); alert(`Error removing link: ${error.message}`); } } }} style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', background: '#dc3545', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > Remove </button> </div> </td> </tr> ); }) ) : ( <tr> <td colSpan="6" style={{ textAlign: 'center', padding: '2rem', color: '#6c757d', }} > No complex dimension links. Click the + button above to add one. </td> </tr> )} </tbody> </table> </div> </> ); }