datajunction-ui 0.0.23-rc.0 → 0.0.26
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 +8 -2
- package/src/app/index.tsx +6 -0
- package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
- package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
- package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
- package/src/app/pages/NamespacePage/index.jsx +489 -62
- package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
- package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
- package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
- package/src/app/pages/Root/index.tsx +5 -0
- package/src/app/services/DJService.js +61 -2
- package/src/styles/index.css +2 -2
- package/src/app/icons/FilterIcon.jsx +0 -7
- package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
- package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
- package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
- package/src/app/pages/NamespacePage/UserSelect.jsx +0 -47
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useParams } from 'react-router-dom';
|
|
3
|
-
import { useContext, useEffect, useState } from 'react';
|
|
2
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { useContext, useEffect, useState, useCallback } from 'react';
|
|
4
4
|
import NodeStatus from '../NodePage/NodeStatus';
|
|
5
5
|
import DJClientContext from '../../providers/djclient';
|
|
6
6
|
import { useCurrentUser } from '../../providers/UserProvider';
|
|
7
7
|
import Explorer from '../NamespacePage/Explorer';
|
|
8
8
|
import AddNodeDropdown from '../../components/AddNodeDropdown';
|
|
9
9
|
import NodeListActions from '../../components/NodeListActions';
|
|
10
|
-
import AddNamespacePopover from './AddNamespacePopover';
|
|
11
|
-
import FilterIcon from '../../icons/FilterIcon';
|
|
12
10
|
import LoadingIcon from '../../icons/LoadingIcon';
|
|
13
|
-
import
|
|
14
|
-
import NodeTypeSelect from './NodeTypeSelect';
|
|
15
|
-
import NodeModeSelect from './NodeModeSelect';
|
|
16
|
-
import TagSelect from './TagSelect';
|
|
11
|
+
import CompactSelect from './CompactSelect';
|
|
17
12
|
|
|
18
13
|
import 'styles/node-list.css';
|
|
19
14
|
import 'styles/sorted-table.css';
|
|
@@ -27,6 +22,142 @@ export function NamespacePage() {
|
|
|
27
22
|
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
28
23
|
const { currentUser } = useCurrentUser();
|
|
29
24
|
var { namespace } = useParams();
|
|
25
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
26
|
+
|
|
27
|
+
// Data for select options
|
|
28
|
+
const [users, setUsers] = useState([]);
|
|
29
|
+
const [tags, setTags] = useState([]);
|
|
30
|
+
const [usersLoading, setUsersLoading] = useState(true);
|
|
31
|
+
const [tagsLoading, setTagsLoading] = useState(true);
|
|
32
|
+
|
|
33
|
+
// Load users and tags for dropdowns
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const fetchUsers = async () => {
|
|
36
|
+
const data = await djClient.users();
|
|
37
|
+
setUsers(data || []);
|
|
38
|
+
setUsersLoading(false);
|
|
39
|
+
};
|
|
40
|
+
const fetchTags = async () => {
|
|
41
|
+
const data = await djClient.listTags();
|
|
42
|
+
setTags(data || []);
|
|
43
|
+
setTagsLoading(false);
|
|
44
|
+
};
|
|
45
|
+
fetchUsers().catch(console.error);
|
|
46
|
+
fetchTags().catch(console.error);
|
|
47
|
+
}, [djClient]);
|
|
48
|
+
|
|
49
|
+
// Parse all filters from URL
|
|
50
|
+
const getFiltersFromUrl = useCallback(
|
|
51
|
+
() => ({
|
|
52
|
+
node_type: searchParams.get('type') || '',
|
|
53
|
+
tags: searchParams.get('tags') ? searchParams.get('tags').split(',') : [],
|
|
54
|
+
edited_by: searchParams.get('editedBy') || '',
|
|
55
|
+
mode: searchParams.get('mode') || '',
|
|
56
|
+
ownedBy: searchParams.get('ownedBy') || '',
|
|
57
|
+
statuses: searchParams.get('statuses') || '',
|
|
58
|
+
missingDescription: searchParams.get('missingDescription') === 'true',
|
|
59
|
+
hasMaterialization: searchParams.get('hasMaterialization') === 'true',
|
|
60
|
+
orphanedDimension: searchParams.get('orphanedDimension') === 'true',
|
|
61
|
+
}),
|
|
62
|
+
[searchParams],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const [filters, setFilters] = useState(getFiltersFromUrl);
|
|
66
|
+
const [moreFiltersOpen, setMoreFiltersOpen] = useState(false);
|
|
67
|
+
|
|
68
|
+
// Sync filters state when URL changes
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
setFilters(getFiltersFromUrl());
|
|
71
|
+
}, [searchParams, getFiltersFromUrl]);
|
|
72
|
+
|
|
73
|
+
// Update URL when filters change
|
|
74
|
+
const updateFilters = useCallback(
|
|
75
|
+
newFilters => {
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
|
|
78
|
+
if (newFilters.node_type) params.set('type', newFilters.node_type);
|
|
79
|
+
if (newFilters.tags?.length)
|
|
80
|
+
params.set('tags', newFilters.tags.join(','));
|
|
81
|
+
if (newFilters.edited_by) params.set('editedBy', newFilters.edited_by);
|
|
82
|
+
if (newFilters.mode) params.set('mode', newFilters.mode);
|
|
83
|
+
if (newFilters.ownedBy) params.set('ownedBy', newFilters.ownedBy);
|
|
84
|
+
if (newFilters.statuses) params.set('statuses', newFilters.statuses);
|
|
85
|
+
if (newFilters.missingDescription)
|
|
86
|
+
params.set('missingDescription', 'true');
|
|
87
|
+
if (newFilters.hasMaterialization)
|
|
88
|
+
params.set('hasMaterialization', 'true');
|
|
89
|
+
if (newFilters.orphanedDimension) params.set('orphanedDimension', 'true');
|
|
90
|
+
|
|
91
|
+
setSearchParams(params);
|
|
92
|
+
},
|
|
93
|
+
[setSearchParams],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const clearAllFilters = () => {
|
|
97
|
+
setSearchParams(new URLSearchParams());
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Check if any filters are active
|
|
101
|
+
const hasActiveFilters =
|
|
102
|
+
filters.node_type ||
|
|
103
|
+
filters.tags?.length ||
|
|
104
|
+
filters.edited_by ||
|
|
105
|
+
filters.mode ||
|
|
106
|
+
filters.ownedBy ||
|
|
107
|
+
filters.statuses ||
|
|
108
|
+
filters.missingDescription ||
|
|
109
|
+
filters.hasMaterialization ||
|
|
110
|
+
filters.orphanedDimension;
|
|
111
|
+
|
|
112
|
+
// Quick presets
|
|
113
|
+
const presets = [
|
|
114
|
+
{
|
|
115
|
+
id: 'my-nodes',
|
|
116
|
+
label: 'My Nodes',
|
|
117
|
+
filters: { ownedBy: currentUser?.username },
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'needs-attention',
|
|
121
|
+
label: 'Needs Attention',
|
|
122
|
+
filters: { ownedBy: currentUser?.username, statuses: 'INVALID' },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'drafts',
|
|
126
|
+
label: 'Drafts',
|
|
127
|
+
filters: { ownedBy: currentUser?.username, mode: 'draft' },
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const applyPreset = preset => {
|
|
132
|
+
const newFilters = {
|
|
133
|
+
node_type: '',
|
|
134
|
+
tags: [],
|
|
135
|
+
edited_by: '',
|
|
136
|
+
mode: preset.filters.mode || '',
|
|
137
|
+
ownedBy: preset.filters.ownedBy || '',
|
|
138
|
+
statuses: preset.filters.statuses || '',
|
|
139
|
+
missingDescription: preset.filters.missingDescription || false,
|
|
140
|
+
hasMaterialization: preset.filters.hasMaterialization || false,
|
|
141
|
+
orphanedDimension: preset.filters.orphanedDimension || false,
|
|
142
|
+
};
|
|
143
|
+
updateFilters(newFilters);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Check if a preset is active
|
|
147
|
+
const isPresetActive = preset => {
|
|
148
|
+
const pf = preset.filters;
|
|
149
|
+
return (
|
|
150
|
+
(pf.ownedBy || '') === (filters.ownedBy || '') &&
|
|
151
|
+
(pf.statuses || '') === (filters.statuses || '') &&
|
|
152
|
+
(pf.mode || '') === (filters.mode || '') &&
|
|
153
|
+
!filters.node_type &&
|
|
154
|
+
!filters.tags?.length &&
|
|
155
|
+
!filters.edited_by &&
|
|
156
|
+
!filters.missingDescription &&
|
|
157
|
+
!filters.hasMaterialization &&
|
|
158
|
+
!filters.orphanedDimension
|
|
159
|
+
);
|
|
160
|
+
};
|
|
30
161
|
|
|
31
162
|
const [state, setState] = useState({
|
|
32
163
|
namespace: namespace ? namespace : '',
|
|
@@ -34,13 +165,6 @@ export function NamespacePage() {
|
|
|
34
165
|
});
|
|
35
166
|
const [retrieved, setRetrieved] = useState(false);
|
|
36
167
|
|
|
37
|
-
const [filters, setFilters] = useState({
|
|
38
|
-
tags: [],
|
|
39
|
-
node_type: '',
|
|
40
|
-
edited_by: '',
|
|
41
|
-
mode: '',
|
|
42
|
-
});
|
|
43
|
-
|
|
44
168
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
45
169
|
|
|
46
170
|
const [sortConfig, setSortConfig] = useState({
|
|
@@ -113,6 +237,16 @@ export function NamespacePage() {
|
|
|
113
237
|
useEffect(() => {
|
|
114
238
|
const fetchData = async () => {
|
|
115
239
|
setRetrieved(false);
|
|
240
|
+
|
|
241
|
+
// Build extended filters for API
|
|
242
|
+
const extendedFilters = {
|
|
243
|
+
ownedBy: filters.ownedBy || null,
|
|
244
|
+
statuses: filters.statuses ? [filters.statuses] : null,
|
|
245
|
+
missingDescription: filters.missingDescription,
|
|
246
|
+
hasMaterialization: filters.hasMaterialization,
|
|
247
|
+
orphanedDimension: filters.orphanedDimension,
|
|
248
|
+
};
|
|
249
|
+
|
|
116
250
|
const nodes = await djClient.listNodesForLanding(
|
|
117
251
|
namespace,
|
|
118
252
|
filters.node_type ? [filters.node_type.toUpperCase()] : [],
|
|
@@ -123,6 +257,7 @@ export function NamespacePage() {
|
|
|
123
257
|
50,
|
|
124
258
|
sortConfig,
|
|
125
259
|
filters.mode ? filters.mode.toUpperCase() : null,
|
|
260
|
+
extendedFilters,
|
|
126
261
|
);
|
|
127
262
|
|
|
128
263
|
setState({
|
|
@@ -152,7 +287,15 @@ export function NamespacePage() {
|
|
|
152
287
|
setRetrieved(true);
|
|
153
288
|
};
|
|
154
289
|
fetchData().catch(console.error);
|
|
155
|
-
}, [
|
|
290
|
+
}, [
|
|
291
|
+
djClient,
|
|
292
|
+
filters,
|
|
293
|
+
before,
|
|
294
|
+
after,
|
|
295
|
+
sortConfig.key,
|
|
296
|
+
sortConfig.direction,
|
|
297
|
+
namespace,
|
|
298
|
+
]);
|
|
156
299
|
|
|
157
300
|
const loadNext = () => {
|
|
158
301
|
if (nextCursor) {
|
|
@@ -167,6 +310,31 @@ export function NamespacePage() {
|
|
|
167
310
|
}
|
|
168
311
|
};
|
|
169
312
|
|
|
313
|
+
// Select options
|
|
314
|
+
const typeOptions = [
|
|
315
|
+
{ value: 'source', label: 'Source' },
|
|
316
|
+
{ value: 'transform', label: 'Transform' },
|
|
317
|
+
{ value: 'dimension', label: 'Dimension' },
|
|
318
|
+
{ value: 'metric', label: 'Metric' },
|
|
319
|
+
{ value: 'cube', label: 'Cube' },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const modeOptions = [
|
|
323
|
+
{ value: 'published', label: 'Published' },
|
|
324
|
+
{ value: 'draft', label: 'Draft' },
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const statusOptions = [
|
|
328
|
+
{ value: 'VALID', label: 'Valid' },
|
|
329
|
+
{ value: 'INVALID', label: 'Invalid' },
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const userOptions = users.map(u => ({
|
|
333
|
+
value: u.username,
|
|
334
|
+
label: u.username,
|
|
335
|
+
}));
|
|
336
|
+
const tagOptions = tags.map(t => ({ value: t.name, label: t.display_name }));
|
|
337
|
+
|
|
170
338
|
const nodesList = retrieved ? (
|
|
171
339
|
state.nodes.length > 0 ? (
|
|
172
340
|
state.nodes.map(node => (
|
|
@@ -234,7 +402,7 @@ export function NamespacePage() {
|
|
|
234
402
|
))
|
|
235
403
|
) : (
|
|
236
404
|
<tr>
|
|
237
|
-
<td>
|
|
405
|
+
<td colSpan={7}>
|
|
238
406
|
<span
|
|
239
407
|
style={{
|
|
240
408
|
display: 'block',
|
|
@@ -243,9 +411,19 @@ export function NamespacePage() {
|
|
|
243
411
|
fontSize: '16px',
|
|
244
412
|
}}
|
|
245
413
|
>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
414
|
+
No nodes found with the current filters.
|
|
415
|
+
{hasActiveFilters && (
|
|
416
|
+
<a
|
|
417
|
+
href="#"
|
|
418
|
+
onClick={e => {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
clearAllFilters();
|
|
421
|
+
}}
|
|
422
|
+
style={{ marginLeft: '0.5rem' }}
|
|
423
|
+
>
|
|
424
|
+
Clear filters
|
|
425
|
+
</a>
|
|
426
|
+
)}
|
|
249
427
|
</span>
|
|
250
428
|
</td>
|
|
251
429
|
</tr>
|
|
@@ -260,63 +438,312 @@ export function NamespacePage() {
|
|
|
260
438
|
</tr>
|
|
261
439
|
);
|
|
262
440
|
|
|
441
|
+
// Count active quality filters (the ones in the "More" dropdown)
|
|
442
|
+
const moreFiltersCount = [
|
|
443
|
+
filters.missingDescription,
|
|
444
|
+
filters.hasMaterialization,
|
|
445
|
+
filters.orphanedDimension,
|
|
446
|
+
].filter(Boolean).length;
|
|
447
|
+
|
|
263
448
|
return (
|
|
264
449
|
<div className="mid">
|
|
265
450
|
<div className="card">
|
|
266
451
|
<div className="card-header">
|
|
267
|
-
<
|
|
268
|
-
|
|
452
|
+
<div
|
|
453
|
+
style={{
|
|
454
|
+
display: 'flex',
|
|
455
|
+
justifyContent: 'space-between',
|
|
456
|
+
alignItems: 'center',
|
|
457
|
+
marginBottom: '1rem',
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
<h2 style={{ margin: 0 }}>Explore</h2>
|
|
461
|
+
<AddNodeDropdown namespace={namespace} />
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
{/* Unified Filter Bar */}
|
|
465
|
+
<div
|
|
466
|
+
style={{
|
|
467
|
+
marginBottom: '1rem',
|
|
468
|
+
padding: '1rem',
|
|
469
|
+
backgroundColor: '#f8f9fa',
|
|
470
|
+
borderRadius: '8px',
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
{/* Top row: Quick presets + Clear all */}
|
|
269
474
|
<div
|
|
270
|
-
className="menu-link"
|
|
271
475
|
style={{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
marginRight: '10px',
|
|
277
|
-
marginLeft: '15px',
|
|
476
|
+
display: 'flex',
|
|
477
|
+
alignItems: 'center',
|
|
478
|
+
gap: '12px',
|
|
479
|
+
marginBottom: '12px',
|
|
278
480
|
}}
|
|
279
481
|
>
|
|
280
|
-
<
|
|
482
|
+
<div
|
|
483
|
+
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
|
|
484
|
+
>
|
|
485
|
+
<span style={{ fontSize: '12px', color: '#555' }}>Quick:</span>
|
|
486
|
+
{presets.map(preset => (
|
|
487
|
+
<button
|
|
488
|
+
key={preset.id}
|
|
489
|
+
onClick={() => applyPreset(preset)}
|
|
490
|
+
style={{
|
|
491
|
+
padding: '4px 10px',
|
|
492
|
+
fontSize: '11px',
|
|
493
|
+
border: '1px solid',
|
|
494
|
+
borderColor: isPresetActive(preset) ? '#1976d2' : '#ddd',
|
|
495
|
+
borderRadius: '12px',
|
|
496
|
+
backgroundColor: isPresetActive(preset)
|
|
497
|
+
? '#e3f2fd'
|
|
498
|
+
: 'white',
|
|
499
|
+
color: isPresetActive(preset) ? '#1976d2' : '#666',
|
|
500
|
+
cursor: 'pointer',
|
|
501
|
+
fontWeight: isPresetActive(preset) ? '600' : '400',
|
|
502
|
+
}}
|
|
503
|
+
>
|
|
504
|
+
{preset.label}
|
|
505
|
+
</button>
|
|
506
|
+
))}
|
|
507
|
+
{hasActiveFilters && (
|
|
508
|
+
<button
|
|
509
|
+
onClick={clearAllFilters}
|
|
510
|
+
style={{
|
|
511
|
+
padding: '4px 10px',
|
|
512
|
+
fontSize: '11px',
|
|
513
|
+
border: 'none',
|
|
514
|
+
backgroundColor: 'transparent',
|
|
515
|
+
color: '#dc3545',
|
|
516
|
+
cursor: 'pointer',
|
|
517
|
+
}}
|
|
518
|
+
>
|
|
519
|
+
Clear all ×
|
|
520
|
+
</button>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
281
523
|
</div>
|
|
524
|
+
|
|
525
|
+
{/* Bottom row: Dropdowns */}
|
|
282
526
|
<div
|
|
283
|
-
className="menu-link"
|
|
284
527
|
style={{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
fontSize: '18px',
|
|
289
|
-
marginRight: '10px',
|
|
528
|
+
display: 'flex',
|
|
529
|
+
alignItems: 'flex-end',
|
|
530
|
+
gap: '12px',
|
|
290
531
|
}}
|
|
291
532
|
>
|
|
292
|
-
|
|
533
|
+
<CompactSelect
|
|
534
|
+
label="Type"
|
|
535
|
+
name="type"
|
|
536
|
+
options={typeOptions}
|
|
537
|
+
value={filters.node_type}
|
|
538
|
+
onChange={e =>
|
|
539
|
+
updateFilters({ ...filters, node_type: e?.value || '' })
|
|
540
|
+
}
|
|
541
|
+
flex={1}
|
|
542
|
+
minWidth="80px"
|
|
543
|
+
testId="select-node-type"
|
|
544
|
+
/>
|
|
545
|
+
<CompactSelect
|
|
546
|
+
label="Tags"
|
|
547
|
+
name="tags"
|
|
548
|
+
options={tagOptions}
|
|
549
|
+
value={filters.tags}
|
|
550
|
+
onChange={e =>
|
|
551
|
+
updateFilters({
|
|
552
|
+
...filters,
|
|
553
|
+
tags: e ? e.map(t => t.value) : [],
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
isMulti
|
|
557
|
+
isLoading={tagsLoading}
|
|
558
|
+
flex={1.5}
|
|
559
|
+
minWidth="100px"
|
|
560
|
+
testId="select-tag"
|
|
561
|
+
/>
|
|
562
|
+
<CompactSelect
|
|
563
|
+
label="Edited By"
|
|
564
|
+
name="editedBy"
|
|
565
|
+
options={userOptions}
|
|
566
|
+
value={filters.edited_by}
|
|
567
|
+
onChange={e =>
|
|
568
|
+
updateFilters({ ...filters, edited_by: e?.value || '' })
|
|
569
|
+
}
|
|
570
|
+
isLoading={usersLoading}
|
|
571
|
+
flex={1}
|
|
572
|
+
minWidth="80px"
|
|
573
|
+
testId="select-user"
|
|
574
|
+
/>
|
|
575
|
+
<CompactSelect
|
|
576
|
+
label="Mode"
|
|
577
|
+
name="mode"
|
|
578
|
+
options={modeOptions}
|
|
579
|
+
value={filters.mode}
|
|
580
|
+
onChange={e =>
|
|
581
|
+
updateFilters({ ...filters, mode: e?.value || '' })
|
|
582
|
+
}
|
|
583
|
+
flex={1}
|
|
584
|
+
minWidth="80px"
|
|
585
|
+
/>
|
|
586
|
+
<CompactSelect
|
|
587
|
+
label="Owner"
|
|
588
|
+
name="owner"
|
|
589
|
+
options={userOptions}
|
|
590
|
+
value={filters.ownedBy}
|
|
591
|
+
onChange={e =>
|
|
592
|
+
updateFilters({ ...filters, ownedBy: e?.value || '' })
|
|
593
|
+
}
|
|
594
|
+
isLoading={usersLoading}
|
|
595
|
+
flex={1}
|
|
596
|
+
minWidth="80px"
|
|
597
|
+
/>
|
|
598
|
+
<CompactSelect
|
|
599
|
+
label="Status"
|
|
600
|
+
name="status"
|
|
601
|
+
options={statusOptions}
|
|
602
|
+
value={filters.statuses}
|
|
603
|
+
onChange={e =>
|
|
604
|
+
updateFilters({ ...filters, statuses: e?.value || '' })
|
|
605
|
+
}
|
|
606
|
+
flex={1}
|
|
607
|
+
minWidth="80px"
|
|
608
|
+
/>
|
|
609
|
+
|
|
610
|
+
{/* More Filters (Quality) */}
|
|
611
|
+
<div style={{ position: 'relative', flex: 0, minWidth: 'auto' }}>
|
|
612
|
+
<div
|
|
613
|
+
style={{
|
|
614
|
+
display: 'flex',
|
|
615
|
+
flexDirection: 'column',
|
|
616
|
+
gap: '2px',
|
|
617
|
+
}}
|
|
618
|
+
>
|
|
619
|
+
<label
|
|
620
|
+
style={{
|
|
621
|
+
fontSize: '10px',
|
|
622
|
+
fontWeight: '600',
|
|
623
|
+
color: '#666',
|
|
624
|
+
textTransform: 'uppercase',
|
|
625
|
+
letterSpacing: '0.5px',
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
Quality
|
|
629
|
+
</label>
|
|
630
|
+
<button
|
|
631
|
+
onClick={() => setMoreFiltersOpen(!moreFiltersOpen)}
|
|
632
|
+
style={{
|
|
633
|
+
height: '32px',
|
|
634
|
+
padding: '0 12px',
|
|
635
|
+
fontSize: '12px',
|
|
636
|
+
border: '1px solid #ccc',
|
|
637
|
+
borderRadius: '4px',
|
|
638
|
+
backgroundColor:
|
|
639
|
+
moreFiltersCount > 0 ? '#e3f2fd' : 'white',
|
|
640
|
+
color: '#666',
|
|
641
|
+
cursor: 'pointer',
|
|
642
|
+
display: 'flex',
|
|
643
|
+
alignItems: 'center',
|
|
644
|
+
gap: '4px',
|
|
645
|
+
whiteSpace: 'nowrap',
|
|
646
|
+
}}
|
|
647
|
+
>
|
|
648
|
+
{moreFiltersCount > 0
|
|
649
|
+
? `${moreFiltersCount} active`
|
|
650
|
+
: 'Issues'}
|
|
651
|
+
<span style={{ fontSize: '8px' }}>
|
|
652
|
+
{moreFiltersOpen ? '▲' : '▼'}
|
|
653
|
+
</span>
|
|
654
|
+
</button>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
{moreFiltersOpen && (
|
|
658
|
+
<div
|
|
659
|
+
style={{
|
|
660
|
+
position: 'absolute',
|
|
661
|
+
top: '100%',
|
|
662
|
+
right: 0,
|
|
663
|
+
marginTop: '4px',
|
|
664
|
+
padding: '12px',
|
|
665
|
+
backgroundColor: 'white',
|
|
666
|
+
border: '1px solid #ddd',
|
|
667
|
+
borderRadius: '8px',
|
|
668
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
669
|
+
zIndex: 1000,
|
|
670
|
+
minWidth: '200px',
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<label
|
|
674
|
+
style={{
|
|
675
|
+
display: 'flex',
|
|
676
|
+
alignItems: 'center',
|
|
677
|
+
gap: '8px',
|
|
678
|
+
fontSize: '12px',
|
|
679
|
+
color: '#444',
|
|
680
|
+
marginBottom: '8px',
|
|
681
|
+
cursor: 'pointer',
|
|
682
|
+
}}
|
|
683
|
+
>
|
|
684
|
+
<input
|
|
685
|
+
type="checkbox"
|
|
686
|
+
checked={filters.missingDescription}
|
|
687
|
+
onChange={e =>
|
|
688
|
+
updateFilters({
|
|
689
|
+
...filters,
|
|
690
|
+
missingDescription: e.target.checked,
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
/>
|
|
694
|
+
Missing Description
|
|
695
|
+
</label>
|
|
696
|
+
<label
|
|
697
|
+
style={{
|
|
698
|
+
display: 'flex',
|
|
699
|
+
alignItems: 'center',
|
|
700
|
+
gap: '8px',
|
|
701
|
+
fontSize: '12px',
|
|
702
|
+
color: '#444',
|
|
703
|
+
marginBottom: '8px',
|
|
704
|
+
cursor: 'pointer',
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
<input
|
|
708
|
+
type="checkbox"
|
|
709
|
+
checked={filters.orphanedDimension}
|
|
710
|
+
onChange={e =>
|
|
711
|
+
updateFilters({
|
|
712
|
+
...filters,
|
|
713
|
+
orphanedDimension: e.target.checked,
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
/>
|
|
717
|
+
Orphaned Dimensions
|
|
718
|
+
</label>
|
|
719
|
+
<label
|
|
720
|
+
style={{
|
|
721
|
+
display: 'flex',
|
|
722
|
+
alignItems: 'center',
|
|
723
|
+
gap: '8px',
|
|
724
|
+
fontSize: '12px',
|
|
725
|
+
color: '#444',
|
|
726
|
+
cursor: 'pointer',
|
|
727
|
+
}}
|
|
728
|
+
>
|
|
729
|
+
<input
|
|
730
|
+
type="checkbox"
|
|
731
|
+
checked={filters.hasMaterialization}
|
|
732
|
+
onChange={e =>
|
|
733
|
+
updateFilters({
|
|
734
|
+
...filters,
|
|
735
|
+
hasMaterialization: e.target.checked,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
/>
|
|
739
|
+
Has Materialization
|
|
740
|
+
</label>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
293
744
|
</div>
|
|
294
|
-
<NodeTypeSelect
|
|
295
|
-
onChange={entry =>
|
|
296
|
-
setFilters({ ...filters, node_type: entry ? entry.value : '' })
|
|
297
|
-
}
|
|
298
|
-
/>
|
|
299
|
-
<TagSelect
|
|
300
|
-
onChange={entry =>
|
|
301
|
-
setFilters({
|
|
302
|
-
...filters,
|
|
303
|
-
tags: entry ? entry.map(tag => tag.value) : [],
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
/>
|
|
307
|
-
<UserSelect
|
|
308
|
-
onChange={entry =>
|
|
309
|
-
setFilters({ ...filters, edited_by: entry ? entry.value : '' })
|
|
310
|
-
}
|
|
311
|
-
currentUser={currentUser?.username}
|
|
312
|
-
/>
|
|
313
|
-
<NodeModeSelect
|
|
314
|
-
onChange={entry =>
|
|
315
|
-
setFilters({ ...filters, mode: entry ? entry.value : '' })
|
|
316
|
-
}
|
|
317
|
-
/>
|
|
318
|
-
<AddNodeDropdown namespace={namespace} />
|
|
319
745
|
</div>
|
|
746
|
+
|
|
320
747
|
<div className="table-responsive">
|
|
321
748
|
<div className={`sidebar`}>
|
|
322
749
|
<div
|