datajunction-ui 0.0.75 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app/components/DashboardCard.jsx +93 -0
- package/src/app/components/NodeComponents.jsx +173 -0
- package/src/app/components/NodeListActions.jsx +8 -3
- package/src/app/components/__tests__/NodeComponents.test.jsx +262 -0
- package/src/app/hooks/__tests__/useWorkspaceData.test.js +533 -0
- package/src/app/hooks/useWorkspaceData.js +357 -0
- package/src/app/index.tsx +6 -0
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +344 -0
- package/src/app/pages/MyWorkspacePage/CollectionsSection.jsx +188 -0
- package/src/app/pages/MyWorkspacePage/Loadable.jsx +6 -0
- package/src/app/pages/MyWorkspacePage/MaterializationsSection.jsx +190 -0
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +342 -0
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +632 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +185 -0
- package/src/app/pages/MyWorkspacePage/NodeList.jsx +46 -0
- package/src/app/pages/MyWorkspacePage/NotificationsSection.jsx +133 -0
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +209 -0
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +295 -0
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +278 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +238 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +389 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +347 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +272 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NodeList.test.jsx +162 -0
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +204 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +556 -0
- package/src/app/pages/MyWorkspacePage/index.jsx +150 -0
- package/src/app/services/DJService.js +323 -2
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import LoadingIcon from '../icons/LoadingIcon';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reusable card component for dashboard sections
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} props
|
|
9
|
+
* @param {string} props.title - Section title
|
|
10
|
+
* @param {string} [props.actionLink] - Optional link URL for "View All" or other action
|
|
11
|
+
* @param {string} [props.actionText] - Text for action link (defaults to "View All →")
|
|
12
|
+
* @param {boolean} [props.loading] - Show loading spinner
|
|
13
|
+
* @param {React.ReactNode} [props.emptyState] - Content to show when empty (if loading is false and no children)
|
|
14
|
+
* @param {React.ReactNode} props.children - Card content
|
|
15
|
+
* @param {Object} [props.cardStyle] - Additional styles for the card wrapper
|
|
16
|
+
* @param {Object} [props.contentStyle] - Additional styles for the content area
|
|
17
|
+
* @param {boolean} [props.showHeader] - Whether to show the header (defaults to true)
|
|
18
|
+
*/
|
|
19
|
+
export function DashboardCard({
|
|
20
|
+
title,
|
|
21
|
+
actionLink,
|
|
22
|
+
actionText = 'View All →',
|
|
23
|
+
loading = false,
|
|
24
|
+
emptyState = null,
|
|
25
|
+
children,
|
|
26
|
+
cardStyle = {},
|
|
27
|
+
contentStyle = {},
|
|
28
|
+
showHeader = true,
|
|
29
|
+
}) {
|
|
30
|
+
// Check if children has actual content (not false, null, undefined)
|
|
31
|
+
const hasContent =
|
|
32
|
+
React.Children.toArray(children).filter(
|
|
33
|
+
child => child !== false && child !== null && child !== undefined,
|
|
34
|
+
).length > 0;
|
|
35
|
+
const showEmptyState = !loading && !hasContent && emptyState;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<section>
|
|
39
|
+
{showHeader && (
|
|
40
|
+
<div className="section-title-row">
|
|
41
|
+
<h2 className="settings-section-title">{title}</h2>
|
|
42
|
+
{actionLink && (
|
|
43
|
+
<Link to={actionLink} style={{ fontSize: '13px' }}>
|
|
44
|
+
{actionText}
|
|
45
|
+
</Link>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<div
|
|
50
|
+
className="settings-card"
|
|
51
|
+
style={{
|
|
52
|
+
padding: '0.75rem',
|
|
53
|
+
...cardStyle,
|
|
54
|
+
...contentStyle,
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{loading ? (
|
|
58
|
+
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
|
59
|
+
<LoadingIcon />
|
|
60
|
+
</div>
|
|
61
|
+
) : showEmptyState ? (
|
|
62
|
+
emptyState
|
|
63
|
+
) : (
|
|
64
|
+
children
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Empty state component for dashboard cards
|
|
73
|
+
*/
|
|
74
|
+
export function DashboardCardEmpty({ icon, message, action }) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
padding: '2rem',
|
|
79
|
+
textAlign: 'center',
|
|
80
|
+
color: '#666',
|
|
81
|
+
fontSize: '12px',
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{icon && (
|
|
85
|
+
<div style={{ fontSize: '24px', marginBottom: '0.5rem' }}>{icon}</div>
|
|
86
|
+
)}
|
|
87
|
+
<p>{message}</p>
|
|
88
|
+
{action}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default DashboardCard;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reusable node type badge component
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} props
|
|
7
|
+
* @param {string} props.type - Node type (e.g., "METRIC", "DIMENSION")
|
|
8
|
+
* @param {('small'|'medium'|'large')} [props.size='medium'] - Badge size
|
|
9
|
+
* @param {boolean} [props.abbreviated=false] - Show only first character
|
|
10
|
+
* @param {Object} [props.style] - Additional styles
|
|
11
|
+
*/
|
|
12
|
+
export function NodeBadge({
|
|
13
|
+
type,
|
|
14
|
+
size = 'medium',
|
|
15
|
+
abbreviated = false,
|
|
16
|
+
style = {},
|
|
17
|
+
}) {
|
|
18
|
+
if (!type) return null;
|
|
19
|
+
|
|
20
|
+
const sizeMap = {
|
|
21
|
+
small: { fontSize: '7px', padding: '0.44em' },
|
|
22
|
+
medium: { fontSize: '9px', padding: '0.44em' },
|
|
23
|
+
large: { fontSize: '11px', padding: '0.44em' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sizeStyles = sizeMap[size] || sizeMap.medium;
|
|
27
|
+
const displayText = abbreviated ? type.charAt(0) : type;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
className={`node_type__${type.toLowerCase()} badge node_type`}
|
|
32
|
+
style={{
|
|
33
|
+
...sizeStyles,
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
...style,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{displayText}
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reusable node link component
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} props
|
|
47
|
+
* @param {Object} props.node - Node object with name and current.displayName
|
|
48
|
+
* @param {('small'|'medium'|'large')} [props.size='medium'] - Link size
|
|
49
|
+
* @param {boolean} [props.showFullName=false] - Show full node name instead of display name
|
|
50
|
+
* @param {boolean} [props.ellipsis=false] - Enable text overflow ellipsis
|
|
51
|
+
* @param {Object} [props.style] - Additional styles
|
|
52
|
+
*/
|
|
53
|
+
export function NodeLink({
|
|
54
|
+
node,
|
|
55
|
+
size = 'medium',
|
|
56
|
+
showFullName = false,
|
|
57
|
+
ellipsis = false,
|
|
58
|
+
style = {},
|
|
59
|
+
}) {
|
|
60
|
+
if (!node?.name) return null;
|
|
61
|
+
|
|
62
|
+
const sizeMap = {
|
|
63
|
+
small: { fontSize: '10px', fontWeight: '500' },
|
|
64
|
+
medium: { fontSize: '12px', fontWeight: '500' },
|
|
65
|
+
large: { fontSize: '13px', fontWeight: '500' },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const sizeStyles = sizeMap[size] || sizeMap.medium;
|
|
69
|
+
const displayName = showFullName
|
|
70
|
+
? node.name
|
|
71
|
+
: node.current?.displayName || node.name.split('.').pop();
|
|
72
|
+
|
|
73
|
+
const ellipsisStyles = ellipsis
|
|
74
|
+
? {
|
|
75
|
+
overflow: 'hidden',
|
|
76
|
+
textOverflow: 'ellipsis',
|
|
77
|
+
whiteSpace: 'nowrap',
|
|
78
|
+
}
|
|
79
|
+
: {};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<a
|
|
83
|
+
href={`/nodes/${node.name}`}
|
|
84
|
+
style={{
|
|
85
|
+
...sizeStyles,
|
|
86
|
+
textDecoration: 'none',
|
|
87
|
+
...ellipsisStyles,
|
|
88
|
+
...style,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{displayName}
|
|
92
|
+
</a>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Combined node display with link and badge
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} props
|
|
100
|
+
* @param {Object} props.node - Node object
|
|
101
|
+
* @param {('small'|'medium'|'large')} [props.size='medium'] - Overall size
|
|
102
|
+
* @param {boolean} [props.showBadge=true] - Show type badge
|
|
103
|
+
* @param {boolean} [props.abbreviatedBadge=false] - Show abbreviated badge
|
|
104
|
+
* @param {boolean} [props.ellipsis=false] - Enable text overflow ellipsis
|
|
105
|
+
* @param {string} [props.gap='6px'] - Gap between link and badge
|
|
106
|
+
* @param {Object} [props.containerStyle] - Additional container styles
|
|
107
|
+
*/
|
|
108
|
+
export function NodeDisplay({
|
|
109
|
+
node,
|
|
110
|
+
size = 'medium',
|
|
111
|
+
showBadge = true,
|
|
112
|
+
abbreviatedBadge = false,
|
|
113
|
+
ellipsis = false,
|
|
114
|
+
gap = '6px',
|
|
115
|
+
containerStyle = {},
|
|
116
|
+
}) {
|
|
117
|
+
if (!node) return null;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
display: 'flex',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
gap,
|
|
125
|
+
...containerStyle,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<NodeLink node={node} size={size} ellipsis={ellipsis} />
|
|
129
|
+
{showBadge && node.type && (
|
|
130
|
+
<NodeBadge
|
|
131
|
+
type={node.type}
|
|
132
|
+
size={size}
|
|
133
|
+
abbreviated={abbreviatedBadge}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Node chip component - compact display with border
|
|
142
|
+
* Used in NeedsAttentionSection
|
|
143
|
+
*
|
|
144
|
+
* @param {Object} props
|
|
145
|
+
* @param {Object} props.node - Node object
|
|
146
|
+
* @param {boolean} [props.abbreviatedBadge=true] - Show abbreviated badge
|
|
147
|
+
*/
|
|
148
|
+
export function NodeChip({ node }) {
|
|
149
|
+
if (!node) return null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<a
|
|
153
|
+
href={`/nodes/${node.name}`}
|
|
154
|
+
style={{
|
|
155
|
+
display: 'inline-flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
gap: '3px',
|
|
158
|
+
padding: '2px 6px',
|
|
159
|
+
fontSize: '10px',
|
|
160
|
+
border: '1px solid var(--border-color, #ddd)',
|
|
161
|
+
borderRadius: '3px',
|
|
162
|
+
textDecoration: 'none',
|
|
163
|
+
color: 'inherit',
|
|
164
|
+
backgroundColor: 'var(--card-bg, #f8f9fa)',
|
|
165
|
+
whiteSpace: 'nowrap',
|
|
166
|
+
flexShrink: 0,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<NodeBadge type={node.type} size="small" abbreviated={true} />
|
|
170
|
+
{node.current?.displayName || node.name.split('.').pop()}
|
|
171
|
+
</a>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -39,14 +39,19 @@ export default function NodeListActions({ nodeName, iconSize = 20 }) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
|
-
<div
|
|
43
|
-
|
|
42
|
+
<div
|
|
43
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
|
44
|
+
>
|
|
45
|
+
<a href={`/nodes/${nodeName}/edit`}>
|
|
44
46
|
<EditIcon size={iconSize} />
|
|
45
47
|
</a>
|
|
46
48
|
<Formik initialValues={initialValues} onSubmit={deleteNode}>
|
|
47
49
|
{function Render({ status, setFieldValue }) {
|
|
48
50
|
return (
|
|
49
|
-
<Form
|
|
51
|
+
<Form
|
|
52
|
+
className="deleteNode"
|
|
53
|
+
style={{ display: 'flex', alignItems: 'flex-start' }}
|
|
54
|
+
>
|
|
50
55
|
{displayMessageAfterSubmit(status)}
|
|
51
56
|
{
|
|
52
57
|
<>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { NodeBadge, NodeLink, NodeDisplay, NodeChip } from '../NodeComponents';
|
|
4
|
+
|
|
5
|
+
describe('NodeComponents', () => {
|
|
6
|
+
describe('<NodeBadge />', () => {
|
|
7
|
+
it('should render badge with node type', () => {
|
|
8
|
+
render(<NodeBadge type="METRIC" />);
|
|
9
|
+
expect(screen.getByText('METRIC')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should return null when no type provided', () => {
|
|
13
|
+
const { container } = render(<NodeBadge />);
|
|
14
|
+
expect(container.firstChild).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should render abbreviated badge', () => {
|
|
18
|
+
render(<NodeBadge type="METRIC" abbreviated={true} />);
|
|
19
|
+
expect(screen.getByText('M')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should apply small size styles', () => {
|
|
23
|
+
render(<NodeBadge type="METRIC" size="small" />);
|
|
24
|
+
const badge = screen.getByText('METRIC');
|
|
25
|
+
expect(badge).toHaveStyle({ fontSize: '7px' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should apply medium size styles', () => {
|
|
29
|
+
render(<NodeBadge type="METRIC" size="medium" />);
|
|
30
|
+
const badge = screen.getByText('METRIC');
|
|
31
|
+
expect(badge).toHaveStyle({ fontSize: '9px' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should apply large size styles', () => {
|
|
35
|
+
render(<NodeBadge type="METRIC" size="large" />);
|
|
36
|
+
const badge = screen.getByText('METRIC');
|
|
37
|
+
expect(badge).toHaveStyle({ fontSize: '11px' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should use medium as default when invalid size provided', () => {
|
|
41
|
+
render(<NodeBadge type="METRIC" size="invalid" />);
|
|
42
|
+
const badge = screen.getByText('METRIC');
|
|
43
|
+
expect(badge).toHaveStyle({ fontSize: '9px' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should apply custom styles', () => {
|
|
47
|
+
render(<NodeBadge type="METRIC" style={{ color: 'red' }} />);
|
|
48
|
+
const badge = screen.getByText('METRIC');
|
|
49
|
+
expect(badge).toHaveStyle({ color: 'red' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should have correct class name', () => {
|
|
53
|
+
render(<NodeBadge type="METRIC" />);
|
|
54
|
+
const badge = screen.getByText('METRIC');
|
|
55
|
+
expect(badge).toHaveClass('node_type__metric');
|
|
56
|
+
expect(badge).toHaveClass('badge');
|
|
57
|
+
expect(badge).toHaveClass('node_type');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('<NodeLink />', () => {
|
|
62
|
+
const mockNode = {
|
|
63
|
+
name: 'default.my_metric',
|
|
64
|
+
type: 'METRIC',
|
|
65
|
+
current: {
|
|
66
|
+
displayName: 'My Metric',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it('should render link with display name', () => {
|
|
71
|
+
render(<NodeLink node={mockNode} />);
|
|
72
|
+
expect(screen.getByText('My Metric')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return null when no node provided', () => {
|
|
76
|
+
const { container } = render(<NodeLink />);
|
|
77
|
+
expect(container.firstChild).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null when node has no name', () => {
|
|
81
|
+
const { container } = render(<NodeLink node={{}} />);
|
|
82
|
+
expect(container.firstChild).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should show full name when showFullName is true', () => {
|
|
86
|
+
render(<NodeLink node={mockNode} showFullName={true} />);
|
|
87
|
+
expect(screen.getByText('default.my_metric')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should use last part of name when no display name', () => {
|
|
91
|
+
const nodeWithoutDisplayName = {
|
|
92
|
+
name: 'default.my_metric',
|
|
93
|
+
type: 'METRIC',
|
|
94
|
+
};
|
|
95
|
+
render(<NodeLink node={nodeWithoutDisplayName} />);
|
|
96
|
+
expect(screen.getByText('my_metric')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should apply ellipsis styles when enabled', () => {
|
|
100
|
+
render(<NodeLink node={mockNode} ellipsis={true} />);
|
|
101
|
+
const link = screen.getByText('My Metric');
|
|
102
|
+
expect(link).toHaveStyle({
|
|
103
|
+
overflow: 'hidden',
|
|
104
|
+
textOverflow: 'ellipsis',
|
|
105
|
+
whiteSpace: 'nowrap',
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not apply ellipsis styles when disabled', () => {
|
|
110
|
+
render(<NodeLink node={mockNode} ellipsis={false} />);
|
|
111
|
+
const link = screen.getByText('My Metric');
|
|
112
|
+
expect(link).not.toHaveStyle({ overflow: 'hidden' });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should apply small size styles', () => {
|
|
116
|
+
render(<NodeLink node={mockNode} size="small" />);
|
|
117
|
+
const link = screen.getByText('My Metric');
|
|
118
|
+
expect(link).toHaveStyle({ fontSize: '10px', fontWeight: '500' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should apply medium size styles', () => {
|
|
122
|
+
render(<NodeLink node={mockNode} size="medium" />);
|
|
123
|
+
const link = screen.getByText('My Metric');
|
|
124
|
+
expect(link).toHaveStyle({ fontSize: '12px', fontWeight: '500' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should apply large size styles', () => {
|
|
128
|
+
render(<NodeLink node={mockNode} size="large" />);
|
|
129
|
+
const link = screen.getByText('My Metric');
|
|
130
|
+
expect(link).toHaveStyle({ fontSize: '13px', fontWeight: '500' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use medium as default when invalid size provided', () => {
|
|
134
|
+
render(<NodeLink node={mockNode} size="invalid" />);
|
|
135
|
+
const link = screen.getByText('My Metric');
|
|
136
|
+
expect(link).toHaveStyle({ fontSize: '12px' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should apply custom styles', () => {
|
|
140
|
+
render(<NodeLink node={mockNode} style={{ color: 'blue' }} />);
|
|
141
|
+
const link = screen.getByText('My Metric');
|
|
142
|
+
expect(link).toHaveStyle({ color: 'blue' });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should link to correct node page', () => {
|
|
146
|
+
render(<NodeLink node={mockNode} />);
|
|
147
|
+
const link = screen.getByText('My Metric');
|
|
148
|
+
expect(link).toHaveAttribute('href', '/nodes/default.my_metric');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('<NodeDisplay />', () => {
|
|
153
|
+
const mockNode = {
|
|
154
|
+
name: 'default.my_metric',
|
|
155
|
+
type: 'METRIC',
|
|
156
|
+
current: {
|
|
157
|
+
displayName: 'My Metric',
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
it('should render both link and badge', () => {
|
|
162
|
+
render(<NodeDisplay node={mockNode} />);
|
|
163
|
+
expect(screen.getByText('My Metric')).toBeInTheDocument();
|
|
164
|
+
expect(screen.getByText('METRIC')).toBeInTheDocument();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return null when no node provided', () => {
|
|
168
|
+
const { container } = render(<NodeDisplay />);
|
|
169
|
+
expect(container.firstChild).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should hide badge when showBadge is false', () => {
|
|
173
|
+
render(<NodeDisplay node={mockNode} showBadge={false} />);
|
|
174
|
+
expect(screen.getByText('My Metric')).toBeInTheDocument();
|
|
175
|
+
expect(screen.queryByText('METRIC')).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should show abbreviated badge', () => {
|
|
179
|
+
render(<NodeDisplay node={mockNode} abbreviatedBadge={true} />);
|
|
180
|
+
expect(screen.getByText('M')).toBeInTheDocument();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should apply ellipsis to link', () => {
|
|
184
|
+
render(<NodeDisplay node={mockNode} ellipsis={true} />);
|
|
185
|
+
const link = screen.getByText('My Metric');
|
|
186
|
+
expect(link).toHaveStyle({ overflow: 'hidden' });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should apply custom gap', () => {
|
|
190
|
+
const { container } = render(<NodeDisplay node={mockNode} gap="10px" />);
|
|
191
|
+
const wrapper = container.firstChild;
|
|
192
|
+
expect(wrapper).toHaveStyle({ gap: '10px' });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should apply custom container styles', () => {
|
|
196
|
+
const { container } = render(
|
|
197
|
+
<NodeDisplay
|
|
198
|
+
node={mockNode}
|
|
199
|
+
containerStyle={{ backgroundColor: 'red' }}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
const wrapper = container.firstChild;
|
|
203
|
+
expect(wrapper).toHaveStyle({ backgroundColor: 'red' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should not show badge when node has no type', () => {
|
|
207
|
+
const nodeWithoutType = {
|
|
208
|
+
name: 'default.my_metric',
|
|
209
|
+
current: { displayName: 'My Metric' },
|
|
210
|
+
};
|
|
211
|
+
render(<NodeDisplay node={nodeWithoutType} />);
|
|
212
|
+
expect(screen.getByText('My Metric')).toBeInTheDocument();
|
|
213
|
+
expect(screen.queryByText('METRIC')).not.toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('<NodeChip />', () => {
|
|
218
|
+
const mockNode = {
|
|
219
|
+
name: 'default.my_metric',
|
|
220
|
+
type: 'METRIC',
|
|
221
|
+
current: {
|
|
222
|
+
displayName: 'My Metric',
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
it('should render chip with badge and name', () => {
|
|
227
|
+
render(<NodeChip node={mockNode} />);
|
|
228
|
+
expect(screen.getByText('M')).toBeInTheDocument(); // abbreviated badge
|
|
229
|
+
expect(screen.getByText('My Metric')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return null when no node provided', () => {
|
|
233
|
+
const { container } = render(<NodeChip />);
|
|
234
|
+
expect(container.firstChild).toBeNull();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should use last part of name when no display name', () => {
|
|
238
|
+
const nodeWithoutDisplayName = {
|
|
239
|
+
name: 'default.my_metric',
|
|
240
|
+
type: 'METRIC',
|
|
241
|
+
};
|
|
242
|
+
render(<NodeChip node={nodeWithoutDisplayName} />);
|
|
243
|
+
expect(screen.getByText('my_metric')).toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should link to correct node page', () => {
|
|
247
|
+
const { container } = render(<NodeChip node={mockNode} />);
|
|
248
|
+
const link = container.querySelector('a');
|
|
249
|
+
expect(link).toHaveAttribute('href', '/nodes/default.my_metric');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should have compact styling', () => {
|
|
253
|
+
const { container } = render(<NodeChip node={mockNode} />);
|
|
254
|
+
const link = container.querySelector('a');
|
|
255
|
+
expect(link).toHaveStyle({
|
|
256
|
+
fontSize: '10px',
|
|
257
|
+
padding: '2px 6px',
|
|
258
|
+
whiteSpace: 'nowrap',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|