datajunction-ui 0.0.23 → 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.
Files changed (25) hide show
  1. package/package.json +11 -4
  2. package/src/app/index.tsx +6 -0
  3. package/src/app/pages/NamespacePage/CompactSelect.jsx +100 -0
  4. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +8 -5
  5. package/src/app/pages/NamespacePage/__tests__/CompactSelect.test.jsx +190 -0
  6. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -8
  7. package/src/app/pages/NamespacePage/index.jsx +489 -62
  8. package/src/app/pages/QueryPlannerPage/Loadable.jsx +6 -0
  9. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +311 -0
  10. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +470 -0
  11. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +384 -0
  12. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +239 -0
  13. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +638 -0
  14. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +429 -0
  15. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +317 -0
  16. package/src/app/pages/QueryPlannerPage/index.jsx +209 -0
  17. package/src/app/pages/QueryPlannerPage/styles.css +1251 -0
  18. package/src/app/pages/Root/index.tsx +5 -0
  19. package/src/app/services/DJService.js +61 -2
  20. package/src/styles/index.css +2 -2
  21. package/src/app/icons/FilterIcon.jsx +0 -7
  22. package/src/app/pages/NamespacePage/FieldControl.jsx +0 -21
  23. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +0 -30
  24. package/src/app/pages/NamespacePage/TagSelect.jsx +0 -44
  25. 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.23",
3
+ "version": "0.0.26",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -180,14 +180,21 @@
180
180
  }
181
181
  },
182
182
  "resolutions": {
183
- "@codemirror/state": "6.2.0",
184
- "@codemirror/view": "6.2.0",
185
- "@lezer/common": "^1.0.0"
183
+ "@lezer/common": "^1.2.0",
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"
186
191
  },
187
192
  "devDependencies": {
188
193
  "@babel/plugin-proposal-class-properties": "7.18.6",
189
194
  "@babel/plugin-proposal-private-property-in-object": "7.21.11",
190
195
  "@testing-library/user-event": "14.4.3",
196
+ "@types/glob": "^8.1.0",
197
+ "@types/minimatch": "^5.1.2",
191
198
  "eslint-config-prettier": "8.8.0",
192
199
  "eslint-plugin-prettier": "4.2.1",
193
200
  "eslint-plugin-react-hooks": "4.6.0",
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
- export default function NodeModeSelect({ onChange }) {
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
+ });