datajunction-ui
Version:
DataJunction Metrics Platform UI
383 lines (359 loc) • 11.2 kB
JSX
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>
);
}