datajunction-ui 0.0.75 → 0.0.77

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/DashboardCard.jsx +93 -0
  3. package/src/app/components/NodeComponents.jsx +173 -0
  4. package/src/app/components/NodeListActions.jsx +8 -3
  5. package/src/app/components/__tests__/NodeComponents.test.jsx +262 -0
  6. package/src/app/hooks/__tests__/useWorkspaceData.test.js +533 -0
  7. package/src/app/hooks/useWorkspaceData.js +357 -0
  8. package/src/app/index.tsx +6 -0
  9. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +344 -0
  10. package/src/app/pages/MyWorkspacePage/CollectionsSection.jsx +188 -0
  11. package/src/app/pages/MyWorkspacePage/Loadable.jsx +6 -0
  12. package/src/app/pages/MyWorkspacePage/MaterializationsSection.jsx +190 -0
  13. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +342 -0
  14. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +632 -0
  15. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +185 -0
  16. package/src/app/pages/MyWorkspacePage/NodeList.jsx +46 -0
  17. package/src/app/pages/MyWorkspacePage/NotificationsSection.jsx +133 -0
  18. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +209 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +295 -0
  20. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +278 -0
  21. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +238 -0
  22. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +389 -0
  23. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +347 -0
  24. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +272 -0
  25. package/src/app/pages/MyWorkspacePage/__tests__/NodeList.test.jsx +162 -0
  26. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +204 -0
  27. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +556 -0
  28. package/src/app/pages/MyWorkspacePage/index.jsx +150 -0
  29. package/src/app/services/DJService.js +323 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -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
- <a href={`/nodes/${nodeName}/edit`} style={{ marginLeft: '0.5rem' }}>
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 className="deleteNode">
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
+ });