UNPKG

datajunction-ui

Version:

DataJunction Metrics Platform UI

383 lines (359 loc) 11.2 kB
import * as React from 'react'; import { useParams } from 'react-router-dom'; import { useContext, useEffect, useState } from 'react'; import NodeStatus from '../NodePage/NodeStatus'; import DJClientContext from '../../providers/djclient'; import Explorer from '../NamespacePage/Explorer'; import AddNodeDropdown from '../../components/AddNodeDropdown'; import NodeListActions from '../../components/NodeListActions'; import AddNamespacePopover from './AddNamespacePopover'; import FilterIcon from '../../icons/FilterIcon'; import LoadingIcon from '../../icons/LoadingIcon'; import UserSelect from './UserSelect'; import NodeTypeSelect from './NodeTypeSelect'; import TagSelect from './TagSelect'; import 'styles/node-list.css'; import 'styles/sorted-table.css'; export function NamespacePage() { const ASC = 'ascending'; const DESC = 'descending'; const fields = ['name', 'displayName', 'type', 'status', 'updatedAt']; const djClient = useContext(DJClientContext).DataJunctionAPI; var { namespace } = useParams(); const [state, setState] = useState({ namespace: namespace ? namespace : '', nodes: [], }); const [retrieved, setRetrieved] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [filters, setFilters] = useState({ tags: [], node_type: '', edited_by: '', }); const [namespaceHierarchy, setNamespaceHierarchy] = useState([]); const [sortConfig, setSortConfig] = useState({ key: 'updatedAt', direction: DESC, }); const [before, setBefore] = useState(null); const [after, setAfter] = useState(null); const [prevCursor, setPrevCursor] = useState(true); const [nextCursor, setNextCursor] = useState(true); const [hasNextPage, setHasNextPage] = useState(true); const [hasPrevPage, setHasPrevPage] = useState(true); const sortedNodes = React.useMemo(() => { let sortableData = [...Object.values(state.nodes)]; if (sortConfig !== null) { sortableData.sort((a, b) => { if ( a[sortConfig.key] < b[sortConfig.key] || a.current[sortConfig.key] < b.current[sortConfig.key] ) { return sortConfig.direction === ASC ? -1 : 1; } if ( a[sortConfig.key] > b[sortConfig.key] || a.current[sortConfig.key] > b.current[sortConfig.key] ) { return sortConfig.direction === ASC ? 1 : -1; } return 0; }); } return sortableData; }, [state.nodes, filters, sortConfig]); const requestSort = key => { let direction = ASC; if (sortConfig.key === key && sortConfig.direction === ASC) { direction = DESC; } setSortConfig({ key, direction }); }; const getClassNamesFor = name => { if (sortConfig.key === name) { return sortConfig.direction; } return undefined; }; const createNamespaceHierarchy = namespaceList => { const hierarchy = []; for (const item of namespaceList) { const namespaces = item.namespace.split('.'); let currentLevel = hierarchy; let path = ''; for (const ns of namespaces) { path += ns; let existingNamespace = currentLevel.find(el => el.namespace === ns); if (!existingNamespace) { existingNamespace = { namespace: ns, children: [], path: path, }; currentLevel.push(existingNamespace); } currentLevel = existingNamespace.children; path += '.'; } } return hierarchy; }; useEffect(() => { const fetchData = async () => { const namespaces = await djClient.namespaces(); const hierarchy = createNamespaceHierarchy(namespaces); setNamespaceHierarchy(hierarchy); const currentUser = await djClient.whoami(); // setFilters({...filters, edited_by: currentUser?.username}); setCurrentUser(currentUser); }; fetchData().catch(console.error); }, [djClient, djClient.namespaces]); useEffect(() => { const fetchData = async () => { setRetrieved(false); const nodes = await djClient.listNodesForLanding( namespace, filters.node_type ? [filters.node_type.toUpperCase()] : [], filters.tags, filters.edited_by, before, after, 50, ); setState({ namespace: namespace, nodes: nodes.data ? nodes.data.findNodesPaginated.edges.map(n => n.node) : [], }); if (nodes.data) { setPrevCursor( nodes.data ? nodes.data.findNodesPaginated.pageInfo.startCursor : '', ); setNextCursor( nodes.data ? nodes.data.findNodesPaginated.pageInfo.endCursor : '', ); setHasPrevPage( nodes.data ? nodes.data.findNodesPaginated.pageInfo.hasPrevPage : false, ); setHasNextPage( nodes.data ? nodes.data.findNodesPaginated.pageInfo.hasNextPage : false, ); } setRetrieved(true); }; fetchData().catch(console.error); }, [djClient, filters, before, after]); const loadNext = () => { if (nextCursor) { setAfter(nextCursor); setBefore(null); } }; const loadPrev = () => { if (prevCursor) { setAfter(null); setBefore(prevCursor); } }; const nodesList = retrieved ? ( sortedNodes.length > 0 ? ( sortedNodes.map(node => ( <tr key={node.name}> <td> <a href={'/nodes/' + node.name} className="link-table"> {node.name} </a> <span className="rounded-pill badge bg-secondary-soft" style={{ marginLeft: '0.5rem' }} > {node.currentVersion} </span> </td> <td> <a href={'/nodes/' + node.name} className="link-table"> {node.type !== 'source' ? node.current.displayName : ''} </a> </td> <td> <span className={ 'node_type__' + node.type.toLowerCase() + ' badge node_type' } > {node.type} </span> </td> <td> <NodeStatus node={node} revalidate={false} /> </td> <td> <span className="status"> {new Date(node.current.updatedAt).toLocaleString('en-us')} </span> </td> <td> <NodeListActions nodeName={node?.name} /> </td> </tr> )) ) : ( <tr> <td> <span style={{ display: 'block', marginTop: '2rem', marginLeft: '2rem', fontSize: '16px', }} > There are no nodes in{' '} <a href={`/namespaces/${namespace}`}>{namespace}</a> with the above filters! </span> </td> </tr> ) ) : ( <tr> <td> <span style={{ display: 'block', marginTop: '2rem' }}> <LoadingIcon /> </span> </td> </tr> ); return ( <div className="mid"> <div className="card"> <div className="card-header"> <h2>Explore</h2> <div className="menu" style={{ margin: '0 0 20px 0' }}> <div className="menu-link" style={{ marginTop: '0.7em', color: '#777', fontFamily: "'Jost'", fontSize: '18px', marginRight: '10px', marginLeft: '15px', }} > <FilterIcon /> </div> <div className="menu-link" style={{ marginTop: '0.6em', color: '#777', fontFamily: "'Jost'", fontSize: '18px', marginRight: '10px', }} > Filter By </div> <NodeTypeSelect onChange={entry => setFilters({ ...filters, node_type: entry ? entry.value : '' }) } /> <TagSelect onChange={entry => setFilters({ ...filters, tags: entry ? entry.map(tag => tag.value) : [], }) } /> <UserSelect onChange={entry => setFilters({ ...filters, edited_by: entry ? entry.value : '' }) } currentUser={currentUser?.username} /> <AddNodeDropdown namespace={namespace} /> </div> <div className="table-responsive"> <div className={`sidebar`}> <span style={{ textTransform: 'uppercase', fontSize: '0.8125rem', fontWeight: '600', color: '#95aac9', padding: '1rem 1rem 1rem 0', }} > Namespaces <AddNamespacePopover namespace={namespace} /> </span> {namespaceHierarchy ? namespaceHierarchy.map(child => ( <Explorer item={child} current={state.namespace} defaultExpand={true} key={child.namespace} /> )) : null} </div> <table className="card-table table"> <thead> <tr> {fields.map(field => { return ( <th key={field}> <button type="button" onClick={() => requestSort(field)} className={'sortable ' + getClassNamesFor(field)} > {field.replace(/([a-z](?=[A-Z]))/g, '$1 ')} </button> </th> ); })} <th>Actions</th> </tr> </thead> <tbody>{nodesList}</tbody> <tfoot> <tr> <td> {retrieved && hasPrevPage ? ( <a onClick={loadPrev} className="previous round pagination" > ← Previous </a> ) : ( '' )} {retrieved && hasNextPage ? ( <a onClick={loadNext} className="next round pagination"> Next → </a> ) : ( '' )} </td> </tr> </tfoot> </table> </div> </div> </div> </div> ); }