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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datajunction-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "DataJunction Metrics Platform UI",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"repository": {
|
|
@@ -181,7 +181,13 @@
|
|
|
181
181
|
},
|
|
182
182
|
"resolutions": {
|
|
183
183
|
"@lezer/common": "^1.2.0",
|
|
184
|
-
"
|
|
184
|
+
"test-exclude": "^7.0.1",
|
|
185
|
+
"string-width": "^4.2.3",
|
|
186
|
+
"string-width-cjs": "npm:string-width@^4.2.3",
|
|
187
|
+
"strip-ansi": "^6.0.1",
|
|
188
|
+
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
|
189
|
+
"wrap-ansi": "^7.0.0",
|
|
190
|
+
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
|
185
191
|
},
|
|
186
192
|
"devDependencies": {
|
|
187
193
|
"@babel/plugin-proposal-class-properties": "7.18.6",
|
package/src/app/index.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { NodePage } from './pages/NodePage';
|
|
|
15
15
|
import RevisionDiff from './pages/NodePage/RevisionDiff';
|
|
16
16
|
import { SQLBuilderPage } from './pages/SQLBuilderPage/Loadable';
|
|
17
17
|
import { CubeBuilderPage } from './pages/CubeBuilderPage/Loadable';
|
|
18
|
+
import { QueryPlannerPage } from './pages/QueryPlannerPage/Loadable';
|
|
18
19
|
import { TagPage } from './pages/TagPage/Loadable';
|
|
19
20
|
import { AddEditNodePage } from './pages/AddEditNodePage/Loadable';
|
|
20
21
|
import { AddEditTagPage } from './pages/AddEditTagPage/Loadable';
|
|
@@ -122,6 +123,11 @@ export function App() {
|
|
|
122
123
|
key="sql"
|
|
123
124
|
element={<SQLBuilderPage />}
|
|
124
125
|
/>
|
|
126
|
+
<Route
|
|
127
|
+
path="materialization-planner"
|
|
128
|
+
key="materialization-planner"
|
|
129
|
+
element={<QueryPlannerPage />}
|
|
130
|
+
/>
|
|
125
131
|
<Route path="tags" key="tags">
|
|
126
132
|
<Route path=":name" element={<TagPage />} />
|
|
127
133
|
</Route>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Select from 'react-select';
|
|
2
|
+
|
|
3
|
+
// Compact select with label above - saves horizontal space
|
|
4
|
+
export default function CompactSelect({
|
|
5
|
+
label,
|
|
6
|
+
name,
|
|
7
|
+
options,
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
isMulti = false,
|
|
11
|
+
isClearable = true,
|
|
12
|
+
placeholder = 'Select...',
|
|
13
|
+
minWidth = '100px',
|
|
14
|
+
flex = 1,
|
|
15
|
+
isLoading = false,
|
|
16
|
+
testId = null,
|
|
17
|
+
}) {
|
|
18
|
+
// For single select, find the matching option
|
|
19
|
+
// For multi select, filter to matching options
|
|
20
|
+
const selectedValue = isMulti
|
|
21
|
+
? value?.length
|
|
22
|
+
? options.filter(o => value.includes(o.value))
|
|
23
|
+
: []
|
|
24
|
+
: value
|
|
25
|
+
? options.find(o => o.value === value)
|
|
26
|
+
: null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column',
|
|
33
|
+
gap: '2px',
|
|
34
|
+
flex,
|
|
35
|
+
minWidth,
|
|
36
|
+
}}
|
|
37
|
+
data-testid={testId}
|
|
38
|
+
>
|
|
39
|
+
<label
|
|
40
|
+
style={{
|
|
41
|
+
fontSize: '10px',
|
|
42
|
+
fontWeight: '600',
|
|
43
|
+
color: '#666',
|
|
44
|
+
textTransform: 'uppercase',
|
|
45
|
+
letterSpacing: '0.5px',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{label}
|
|
49
|
+
</label>
|
|
50
|
+
<Select
|
|
51
|
+
name={name}
|
|
52
|
+
isClearable={isClearable}
|
|
53
|
+
isMulti={isMulti}
|
|
54
|
+
isLoading={isLoading}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
value={selectedValue}
|
|
58
|
+
styles={{
|
|
59
|
+
control: base => ({
|
|
60
|
+
...base,
|
|
61
|
+
minHeight: '32px',
|
|
62
|
+
height: isMulti ? 'auto' : '32px',
|
|
63
|
+
fontSize: '12px',
|
|
64
|
+
backgroundColor: 'white',
|
|
65
|
+
}),
|
|
66
|
+
valueContainer: base => ({
|
|
67
|
+
...base,
|
|
68
|
+
padding: '0 6px',
|
|
69
|
+
}),
|
|
70
|
+
input: base => ({
|
|
71
|
+
...base,
|
|
72
|
+
margin: 0,
|
|
73
|
+
padding: 0,
|
|
74
|
+
}),
|
|
75
|
+
indicatorSeparator: () => ({
|
|
76
|
+
display: 'none',
|
|
77
|
+
}),
|
|
78
|
+
dropdownIndicator: base => ({
|
|
79
|
+
...base,
|
|
80
|
+
padding: '4px',
|
|
81
|
+
}),
|
|
82
|
+
clearIndicator: base => ({
|
|
83
|
+
...base,
|
|
84
|
+
padding: '4px',
|
|
85
|
+
}),
|
|
86
|
+
option: base => ({
|
|
87
|
+
...base,
|
|
88
|
+
fontSize: '12px',
|
|
89
|
+
padding: '6px 10px',
|
|
90
|
+
}),
|
|
91
|
+
multiValue: base => ({
|
|
92
|
+
...base,
|
|
93
|
+
fontSize: '11px',
|
|
94
|
+
}),
|
|
95
|
+
}}
|
|
96
|
+
options={options}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import Select from 'react-select';
|
|
2
2
|
import Control from './FieldControl';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: 'published', label: 'Published' },
|
|
6
|
+
{ value: 'draft', label: 'Draft' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default function NodeModeSelect({ onChange, value }) {
|
|
5
10
|
return (
|
|
6
11
|
<span
|
|
7
12
|
className="menu-link"
|
|
@@ -14,13 +19,11 @@ export default function NodeModeSelect({ onChange }) {
|
|
|
14
19
|
label="Mode"
|
|
15
20
|
components={{ Control }}
|
|
16
21
|
onChange={e => onChange(e)}
|
|
22
|
+
value={value ? options.find(o => o.value === value) : null}
|
|
17
23
|
styles={{
|
|
18
24
|
control: styles => ({ ...styles, backgroundColor: 'white' }),
|
|
19
25
|
}}
|
|
20
|
-
options={
|
|
21
|
-
{ value: 'published', label: 'Published' },
|
|
22
|
-
{ value: 'draft', label: 'Draft' },
|
|
23
|
-
]}
|
|
26
|
+
options={options}
|
|
24
27
|
/>
|
|
25
28
|
</span>
|
|
26
29
|
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import CompactSelect from '../CompactSelect';
|
|
5
|
+
|
|
6
|
+
describe('<CompactSelect />', () => {
|
|
7
|
+
const defaultOptions = [
|
|
8
|
+
{ value: 'option1', label: 'Option 1' },
|
|
9
|
+
{ value: 'option2', label: 'Option 2' },
|
|
10
|
+
{ value: 'option3', label: 'Option 3' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const defaultProps = {
|
|
14
|
+
label: 'Test Label',
|
|
15
|
+
name: 'test-select',
|
|
16
|
+
options: defaultOptions,
|
|
17
|
+
value: '',
|
|
18
|
+
onChange: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders without crashing', () => {
|
|
26
|
+
render(<CompactSelect {...defaultProps} />);
|
|
27
|
+
expect(screen.getByText('Test Label')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('displays the label correctly', () => {
|
|
31
|
+
render(<CompactSelect {...defaultProps} label="My Custom Label" />);
|
|
32
|
+
expect(screen.getByText('My Custom Label')).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows placeholder when no value is selected', () => {
|
|
36
|
+
render(<CompactSelect {...defaultProps} placeholder="Choose one..." />);
|
|
37
|
+
expect(screen.getByText('Choose one...')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('displays the selected value for single select', () => {
|
|
41
|
+
render(<CompactSelect {...defaultProps} value="option1" />);
|
|
42
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('calls onChange when an option is selected', async () => {
|
|
46
|
+
const handleChange = jest.fn();
|
|
47
|
+
render(<CompactSelect {...defaultProps} onChange={handleChange} />);
|
|
48
|
+
|
|
49
|
+
// Open the dropdown
|
|
50
|
+
const selectInput = screen.getByRole('combobox');
|
|
51
|
+
fireEvent.keyDown(selectInput, { key: 'ArrowDown' });
|
|
52
|
+
|
|
53
|
+
// Click on an option
|
|
54
|
+
await waitFor(() => {
|
|
55
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
fireEvent.click(screen.getByText('Option 2'));
|
|
58
|
+
|
|
59
|
+
expect(handleChange).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({ value: 'option2', label: 'Option 2' }),
|
|
61
|
+
expect.anything(),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('supports multi-select mode', async () => {
|
|
66
|
+
const handleChange = jest.fn();
|
|
67
|
+
render(
|
|
68
|
+
<CompactSelect
|
|
69
|
+
{...defaultProps}
|
|
70
|
+
isMulti={true}
|
|
71
|
+
value={['option1']}
|
|
72
|
+
onChange={handleChange}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Should show the selected value
|
|
77
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('displays multiple selected values in multi-select mode', () => {
|
|
81
|
+
render(
|
|
82
|
+
<CompactSelect
|
|
83
|
+
{...defaultProps}
|
|
84
|
+
isMulti={true}
|
|
85
|
+
value={['option1', 'option2']}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('shows loading state when isLoading is true', () => {
|
|
94
|
+
render(<CompactSelect {...defaultProps} isLoading={true} />);
|
|
95
|
+
// react-select shows a loading indicator when isLoading is true
|
|
96
|
+
expect(document.querySelector('.css-1dimb5e-singleValue')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('allows clearing the selection when isClearable is true', async () => {
|
|
100
|
+
const handleChange = jest.fn();
|
|
101
|
+
render(
|
|
102
|
+
<CompactSelect
|
|
103
|
+
{...defaultProps}
|
|
104
|
+
value="option1"
|
|
105
|
+
onChange={handleChange}
|
|
106
|
+
isClearable={true}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Find and click the clear button
|
|
111
|
+
const clearButton = document.querySelector(
|
|
112
|
+
'[class*="indicatorContainer"]:first-of-type',
|
|
113
|
+
);
|
|
114
|
+
if (clearButton) {
|
|
115
|
+
fireEvent.mouseDown(clearButton);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('respects minWidth prop', () => {
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<CompactSelect {...defaultProps} minWidth="200px" />,
|
|
122
|
+
);
|
|
123
|
+
const wrapper = container.firstChild;
|
|
124
|
+
expect(wrapper).toHaveStyle({ minWidth: '200px' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('respects flex prop', () => {
|
|
128
|
+
const { container } = render(<CompactSelect {...defaultProps} flex={2} />);
|
|
129
|
+
const wrapper = container.firstChild;
|
|
130
|
+
expect(wrapper).toHaveStyle({ flex: '2' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('handles empty options array', () => {
|
|
134
|
+
render(<CompactSelect {...defaultProps} options={[]} />);
|
|
135
|
+
expect(screen.getByText('Test Label')).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('handles null value gracefully', () => {
|
|
139
|
+
render(<CompactSelect {...defaultProps} value={null} />);
|
|
140
|
+
expect(screen.getByText('Select...')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles undefined value gracefully', () => {
|
|
144
|
+
render(<CompactSelect {...defaultProps} value={undefined} />);
|
|
145
|
+
expect(screen.getByText('Select...')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders with custom placeholder', () => {
|
|
149
|
+
render(<CompactSelect {...defaultProps} placeholder="Pick something..." />);
|
|
150
|
+
expect(screen.getByText('Pick something...')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('uses default placeholder when none provided', () => {
|
|
154
|
+
render(<CompactSelect {...defaultProps} />);
|
|
155
|
+
expect(screen.getByText('Select...')).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('applies compact styling with reduced height', () => {
|
|
159
|
+
const { container } = render(<CompactSelect {...defaultProps} />);
|
|
160
|
+
// The control element should have compact styling
|
|
161
|
+
const control = container.querySelector('[class*="control"]');
|
|
162
|
+
expect(control).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('opens dropdown on click', async () => {
|
|
166
|
+
render(<CompactSelect {...defaultProps} />);
|
|
167
|
+
|
|
168
|
+
const selectInput = screen.getByRole('combobox');
|
|
169
|
+
fireEvent.mouseDown(selectInput);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
// Menu should be visible with options
|
|
173
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
|
174
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
175
|
+
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('filters options based on user input', async () => {
|
|
180
|
+
render(<CompactSelect {...defaultProps} />);
|
|
181
|
+
|
|
182
|
+
const selectInput = screen.getByRole('combobox');
|
|
183
|
+
fireEvent.focus(selectInput);
|
|
184
|
+
await userEvent.type(selectInput, '2');
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|