UNPKG

datajunction-ui

Version:
538 lines (517 loc) 19.9 kB
import { useContext, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import DJClientContext from '../../providers/djclient'; import { ErrorMessage, Field, Form, Formik } from 'formik'; import { FormikSelect } from '../AddEditNodePage/FormikSelect'; import EditIcon from '../../icons/EditIcon'; import { displayMessageAfterSubmit } from '../../../utils/form'; import LoadingIcon from '../../icons/LoadingIcon'; export default function ManageDimensionLinksDialog({ column, node, dimensions, fkLinks, referenceLink, onSubmit, }) { const djClient = useContext(DJClientContext).DataJunctionAPI; const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState('fk'); const ref = useRef(null); const [dimensionColumns, setDimensionColumns] = useState([]); const [selectedDimension, setSelectedDimension] = useState( referenceLink?.dimension || '', ); useEffect(() => { const handleClickOutside = event => { if (ref.current && !ref.current.contains(event.target)) { setIsOpen(false); } }; document.addEventListener('click', handleClickOutside, true); return () => { document.removeEventListener('click', handleClickOutside, true); }; }, []); // Fetch columns for the selected dimension useEffect(() => { const fetchDimensionColumns = async () => { if (selectedDimension) { try { const dimensionNode = await djClient.node(selectedDimension); const columns = dimensionNode.columns.map(col => ({ value: col.name, label: col.display_name || col.name, })); setDimensionColumns(columns); } catch (error) { console.error('Failed to fetch dimension columns:', error); } } }; fetchDimensionColumns(); }, [selectedDimension, djClient]); const handleFKSubmit = async (values, { setSubmitting, setStatus }) => { try { // Add FK links that are not already present const existingDims = new Set(fkLinks); const newDims = new Set(values.fkDimensions); // Links to add const toAdd = Array.from(newDims).filter(dim => !existingDims.has(dim)); // Links to remove const toRemove = Array.from(existingDims).filter( dim => !newDims.has(dim), ); const addPromises = toAdd.map(dim => djClient.linkDimension(node.name, column.name, dim), ); const removePromises = toRemove.map(dim => djClient.unlinkDimension(node.name, column.name, dim), ); const results = await Promise.all([...addPromises, ...removePromises]); const failures = results.filter( r => !(r.status === 200 || r.status === 201), ); if (failures.length > 0) { const message = failures[0].json?.message || failures[0].json?.detail || 'Failed to update FK links'; setStatus({ failure: message }); return; } setStatus({ success: 'FK links updated successfully!' }); setTimeout(() => { onSubmit(); }, 500); } catch (error) { setStatus({ failure: error.message }); } finally { setSubmitting(false); } }; const handleReferenceSubmit = async ( values, { setSubmitting, setStatus }, ) => { try { const response = await djClient.addReferenceDimensionLink( node.name, column.name, values.dimensionNode, values.dimensionColumn, values.role || null, ); if (response.status === 200 || response.status === 201) { setStatus({ success: 'Reference link updated successfully!' }); setTimeout(() => { onSubmit(); }, 500); } else { setStatus({ failure: response.json?.message || 'Failed to add link' }); } } catch (error) { setStatus({ failure: error.message }); } finally { setSubmitting(false); } }; const handleRemoveReference = async () => { if ( !window.confirm('Are you sure you want to remove this reference link?') ) { return; } try { const response = await djClient.removeReferenceDimensionLink( node.name, column.name, ); if (response.status === 200 || response.status === 201) { alert('Reference dimension link removed successfully!'); window.location.reload(); } else { alert(response.json?.message || 'Failed to remove link'); } } catch (error) { alert(error.message); } }; return ( <> <button className="edit_button dimension-link-edit" aria-label="ManageDimensionLinksToggle" tabIndex="0" onClick={() => setIsOpen(!isOpen)} title="Manage dimension links for this column" style={{ marginLeft: '0.35rem', padding: '0', opacity: 0.5, transition: 'opacity 0.2s ease', background: 'transparent', border: 'none', cursor: 'pointer', fontSize: '12px', color: '#999', display: 'inline-flex', alignItems: 'center', }} > <EditIcon /> </button> {isOpen && ( <> {/* Backdrop overlay */} <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem', }} onClick={() => setIsOpen(false)} > {/* Modal */} <div role="dialog" aria-label="ManageDimensionLinksDialog" ref={ref} onClick={e => e.stopPropagation()} style={{ backgroundColor: 'white', borderRadius: '8px', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)', width: '550px', maxWidth: '100%', maxHeight: '100%', overflow: 'visible', padding: '1.5rem', textTransform: 'none', fontWeight: 'normal', position: 'relative', zIndex: 1000, }} > <div style={{ maxHeight: 'calc(85vh - 4rem)', overflowY: 'auto' }} > <div style={{ display: 'flex', marginBottom: '1rem' }}> <h3 style={{ margin: '0 1em 0 0', fontSize: '1.25rem', fontWeight: '600', }} > Manage Dimension Links </h3> <span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', }} > <span style={{ fontSize: '1rem', fontWeight: '500', color: '#333', }} > {column.name} </span> <span className="rounded-pill badge bg-secondary-soft" style={{ fontSize: '0.75rem', fontWeight: 'normal' }} > {column.type} </span> </span> </div> {/* Tab Navigation */} <div style={{ display: 'flex', borderBottom: '2px solid #e0e0e0', marginBottom: '1rem', }} > <button type="button" onClick={() => setActiveTab('fk')} style={{ flex: 1, padding: '0.5rem 1rem', border: 'none', background: 'transparent', borderBottom: activeTab === 'fk' ? '3px solid #007bff' : 'none', color: activeTab === 'fk' ? '#007bff' : '#6c757d', fontWeight: activeTab === 'fk' ? 'bold' : 'normal', cursor: 'pointer', }} > FK Links </button> <button type="button" onClick={() => setActiveTab('reference')} style={{ flex: 1, padding: '0.5rem 1rem', border: 'none', background: 'transparent', borderBottom: activeTab === 'reference' ? '3px solid #007bff' : 'none', color: activeTab === 'reference' ? '#007bff' : '#6c757d', fontWeight: activeTab === 'reference' ? 'bold' : 'normal', cursor: 'pointer', }} > Reference Links </button> </div> {/* FK Links Tab */} {activeTab === 'fk' && ( <div> <p style={{ fontSize: '0.875rem', color: '#6c757d', marginBottom: '1rem', }} > FK Links automatically join via the dimension's primary key. Select one or more dimensions to link to this column. </p> <Formik initialValues={{ fkDimensions: fkLinks, }} onSubmit={handleFKSubmit} > {function Render({ isSubmitting, status }) { return ( <Form> {displayMessageAfterSubmit(status)} <div style={{ marginBottom: '1rem' }}> <label htmlFor="fkDimensions"> Select Dimensions </label> <FormikSelect selectOptions={dimensions} formikFieldName="fkDimensions" placeholder="Select dimensions" isMulti={true} defaultValue={fkLinks.map(dim => ({ value: dim, label: dim, }))} menuPosition="fixed" /> </div> <button className="add_node" type="submit" disabled={isSubmitting} style={{ marginLeft: '0' }} > {isSubmitting ? <LoadingIcon /> : 'Save'} </button> </Form> ); }} </Formik> </div> )} {/* Reference Links Tab */} {activeTab === 'reference' && ( <div> <p style={{ fontSize: '0.875rem', color: '#6c757d', marginBottom: '1rem', }} > Reference Links explicitly map this column to a specific dimension attribute. Use when the relationship is not through a primary key. </p> <Formik initialValues={{ dimensionNode: referenceLink?.dimension || '', dimensionColumn: referenceLink?.dimension_column || '', role: referenceLink?.role || '', }} onSubmit={handleReferenceSubmit} validate={values => { const errors = {}; if (!values.dimensionNode) { errors.dimensionNode = 'Required'; } if (!values.dimensionColumn) { errors.dimensionColumn = 'Required'; } return errors; }} > {function Render({ isSubmitting, status, setFieldValue, }) { return ( <Form> {displayMessageAfterSubmit(status)} <div style={{ marginBottom: '1rem' }}> <ErrorMessage name="dimensionNode" component="span" /> <label htmlFor="dimensionNode"> Dimension Node * </label> <FormikSelect selectOptions={dimensions} formikFieldName="dimensionNode" placeholder="Select dimension" defaultValue={ referenceLink ? { value: referenceLink.dimension, label: referenceLink.dimension, } : null } onChange={option => { setFieldValue( 'dimensionNode', option?.value || '', ); setSelectedDimension(option?.value || ''); setFieldValue('dimensionColumn', ''); }} /> </div> <div style={{ marginBottom: '1rem' }}> <ErrorMessage name="dimensionColumn" component="span" /> <label htmlFor="dimensionColumn"> Dimension Column * </label> {dimensionColumns.length > 0 ? ( <FormikSelect selectOptions={dimensionColumns} formikFieldName="dimensionColumn" placeholder="Select column" defaultValue={ referenceLink ? { value: referenceLink.dimension_column, label: referenceLink.dimension_column, } : null } /> ) : ( <Field type="text" name="dimensionColumn" id="dimensionColumn" placeholder="Enter dimension column name" style={{ width: '100%', padding: '0.5rem', }} /> )} </div> <div style={{ marginBottom: '1rem' }}> <label htmlFor="role">Role (Optional)</label> <Field type="text" name="role" id="role" placeholder="e.g., birth_date, registration_date" style={{ width: '100%', padding: '0.5rem', }} /> <small style={{ color: '#6c757d', fontSize: '0.75rem', }} > Optional role if linking the same dimension multiple times </small> </div> <div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem', }} > <button className="add_node" type="submit" disabled={isSubmitting} > {isSubmitting ? ( <LoadingIcon /> ) : referenceLink ? ( 'Update Link' ) : ( 'Add Link' )} </button> {referenceLink && ( <button type="button" onClick={handleRemoveReference} style={{ padding: '0.5rem 1rem', background: '#dc3545', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', textTransform: 'none', }} > Remove Link </button> )} </div> </Form> ); }} </Formik> </div> )} </div> </div> </div> </> )} </> ); }