datajunction-ui
Version:
DataJunction UI
206 lines (192 loc) • 6.6 kB
JSX
import * as React from 'react';
import { Link } from 'react-router-dom';
import { NodeBadge, NodeLink } from '../../components/NodeComponents';
import NodeListActions from '../../components/NodeListActions';
// Format timestamp to relative time
function formatRelativeTime(dateStr) {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 60) return `${diffMins}m`;
if (diffHours < 24) return `${diffHours}h`;
if (diffDays < 30) return `${diffDays}d`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
// Capitalize first letter
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Type Card Component
function TypeCard({ type, nodes, hasMore, username, activeTab }) {
const displayNodes = nodes;
// Build filter URL based on active tab and type
const getFilterUrl = () => {
const params = new URLSearchParams();
if (activeTab === 'owned') {
params.append('ownedBy', username);
params.append('type', type);
} else if (activeTab === 'watched') {
// For watched, we'd need a different filter - for now use type only
params.append('type', type);
} else if (activeTab === 'edited') {
params.append('updatedBy', username);
params.append('type', type);
}
return `/?${params.toString()}`;
};
return (
<div className="type-group-card">
<div className="type-group-header">
<span className="type-group-title">{capitalize(type)}s</span>
</div>
<div className="type-group-nodes">
{displayNodes.map(node => {
const isInvalid = node.status === 'invalid';
const isDraft = node.mode === 'draft';
const gitRepo = node.gitInfo?.repo;
const gitBranch = node.gitInfo?.branch;
const defaultBranch = node.gitInfo?.defaultBranch;
const isDefaultBranch = gitBranch && gitBranch === defaultBranch;
const hasGitInfo = gitRepo || gitBranch;
return (
<div key={node.name} className="type-group-node">
<NodeBadge type={node.type} size="small" abbreviated={true} />
{isInvalid && (
<span
className="type-group-node-status-icon"
title="Invalid node"
>
⚠️
</span>
)}
{isDraft && (
<span
className="type-group-node-status-icon"
title="Draft mode"
>
📝
</span>
)}
<NodeLink
node={node}
size="small"
ellipsis={true}
style={{ flex: 1, minWidth: 0 }}
/>
{hasGitInfo && (
<span
className={`type-group-node-git-info ${
isDefaultBranch ? 'type-group-node-git-info--default' : ''
}`}
title={`${gitRepo || ''}${gitRepo && gitBranch ? ' / ' : ''}${
gitBranch || ''
}${isDefaultBranch ? ' (default)' : ''}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
maxWidth: '40%',
}}
>
{isDefaultBranch && (
<span style={{ lineHeight: 1, flexShrink: 0 }}>⭐</span>
)}
{gitRepo && (
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
}}
>
{gitRepo}
</span>
)}
{gitRepo && gitBranch && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0 }}
>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
)}
{gitBranch && (
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
}}
>
{gitBranch}
</span>
)}
</span>
)}
{node.current?.updatedAt && (
<span className="type-group-node-time">
{formatRelativeTime(node.current.updatedAt)}
</span>
)}
<div className="type-group-node-actions">
<NodeListActions nodeName={node.name} />
</div>
</div>
);
})}
</div>
{hasMore && (
<Link to={getFilterUrl()} className="type-group-more">
More →
</Link>
)}
</div>
);
}
// Type Group Grid Component (2-column layout)
export function TypeGroupGrid({ groupedData, username, activeTab }) {
if (!groupedData || groupedData.length === 0) {
return (
<div
style={{
padding: '1rem',
textAlign: 'center',
color: '#666',
fontSize: '12px',
}}
>
No nodes to display
</div>
);
}
return (
<div className="type-group-grid">
{groupedData.map(group => (
<TypeCard
key={group.type}
type={group.type}
nodes={group.nodes}
hasMore={group.hasMore}
username={username}
activeTab={activeTab}
/>
))}
</div>
);
}