mirror of
https://github.com/apache/superset.git
synced 2026-07-02 12:55:35 +00:00
Compare commits
23 Commits
fix-mysql-
...
docs/testi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d2c332165 | ||
|
|
572f3392d7 | ||
|
|
90f281f585 | ||
|
|
d62249d13f | ||
|
|
ff102aadb3 | ||
|
|
82e2bc6181 | ||
|
|
784ff82847 | ||
|
|
027b25e6b8 | ||
|
|
b652fab042 | ||
|
|
77a5969dc1 | ||
|
|
fb9032c05c | ||
|
|
7a9dbfe879 | ||
|
|
0de78d8203 | ||
|
|
abc2d46fed | ||
|
|
927cc1cda1 | ||
|
|
7f3840557a | ||
|
|
0defcb604b | ||
|
|
94686ddfbe | ||
|
|
ec322dfd8d | ||
|
|
cb88d886c7 | ||
|
|
608e3baf43 | ||
|
|
b6f6b75348 | ||
|
|
a5ad1d186c |
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -1 +1 @@
|
|||||||
../LLMS.md
|
../AGENTS.md
|
||||||
@@ -82,6 +82,7 @@ intro_header.txt
|
|||||||
|
|
||||||
# for LLMs
|
# for LLMs
|
||||||
llm-context.md
|
llm-context.md
|
||||||
|
AGENTS.md
|
||||||
LLMS.md
|
LLMS.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
CURSOR.md
|
CURSOR.md
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ superset/
|
|||||||
|
|
||||||
### Apache License Headers
|
### Apache License Headers
|
||||||
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
||||||
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
||||||
|
|
||||||
|
### Code Comments
|
||||||
|
- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
|
||||||
|
- **Write timeless comments** - Comments should remain accurate regardless of when they're read
|
||||||
|
|
||||||
## Documentation Requirements
|
## Documentation Requirements
|
||||||
|
|
||||||
@@ -98,6 +102,17 @@ superset/
|
|||||||
- **`selectOption()`** - Select component helper
|
- **`selectOption()`** - Select component helper
|
||||||
- **React Testing Library** - NO Enzyme (removed)
|
- **React Testing Library** - NO Enzyme (removed)
|
||||||
|
|
||||||
|
### Test Structure Guidelines
|
||||||
|
- **Use `test()` instead of `describe()` and `it()`** - Follow the [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principle
|
||||||
|
- **Why**: Reduces unnecessary nesting, improves test isolation, and makes tests more readable
|
||||||
|
- **Pattern**: Write flat test files with descriptive test names that fully describe what's being tested
|
||||||
|
- **Example**: Instead of nested `describe('Component', () => { it('should render', ...) })`, use `test('Component renders correctly', ...)`
|
||||||
|
- **Benefits**:
|
||||||
|
- Each test stands alone with a clear, searchable name
|
||||||
|
- Easier to run individual tests
|
||||||
|
- Forces you to write more descriptive test names
|
||||||
|
- Reduces cognitive overhead from nested context switching
|
||||||
|
|
||||||
### Test Database Patterns
|
### Test Database Patterns
|
||||||
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
|
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
|
||||||
- **API tests**: Update expected columns when adding new model fields
|
- **API tests**: Update expected columns when adding new model fields
|
||||||
@@ -413,13 +413,6 @@ module.exports = {
|
|||||||
'icons/no-fa-icons-usage': 'error',
|
'icons/no-fa-icons-usage': 'error',
|
||||||
'i18n-strings/no-template-vars': ['error', true],
|
'i18n-strings/no-template-vars': ['error', true],
|
||||||
'i18n-strings/sentence-case-buttons': 'error',
|
'i18n-strings/sentence-case-buttons': 'error',
|
||||||
camelcase: [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
allow: ['^UNSAFE_'],
|
|
||||||
properties: 'never',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'class-methods-use-this': 0,
|
'class-methods-use-this': 0,
|
||||||
curly: 2,
|
curly: 2,
|
||||||
'func-names': 0,
|
'func-names': 0,
|
||||||
|
|||||||
1
superset-frontend/.gitignore
vendored
1
superset-frontend/.gitignore
vendored
@@ -3,3 +3,4 @@ cypress/screenshots
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
src/temp
|
src/temp
|
||||||
.temp_cache/
|
.temp_cache/
|
||||||
|
.tsbuildinfo
|
||||||
|
|||||||
8
superset-frontend/package-lock.json
generated
8
superset-frontend/package-lock.json
generated
@@ -8886,9 +8886,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@ndelangen/get-tarball/node_modules/tar-fs": {
|
"node_modules/@ndelangen/get-tarball/node_modules/tar-fs": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -60740,7 +60740,7 @@
|
|||||||
},
|
},
|
||||||
"packages/superset-core": {
|
"packages/superset-core": {
|
||||||
"name": "@apache-superset/core",
|
"name": "@apache-superset/core",
|
||||||
"version": "0.0.1-rc4",
|
"version": "0.0.1-rc5",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { getColorBreakpointsBuckets } from './utils';
|
import { getColorBreakpointsBuckets, getBreakPoints } from './utils';
|
||||||
import { ColorBreakpointType } from './types';
|
import { ColorBreakpointType } from './types';
|
||||||
|
|
||||||
describe('getColorBreakpointsBuckets', () => {
|
describe('getColorBreakpointsBuckets', () => {
|
||||||
@@ -44,3 +44,447 @@ describe('getColorBreakpointsBuckets', () => {
|
|||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getBreakPoints', () => {
|
||||||
|
const accessor = (d: any) => d.value;
|
||||||
|
|
||||||
|
describe('automatic breakpoint generation', () => {
|
||||||
|
it('generates correct number of breakpoints for given buckets', () => {
|
||||||
|
const features = [{ value: 0 }, { value: 50 }, { value: 100 }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toHaveLength(6); // n buckets = n+1 breakpoints
|
||||||
|
expect(breakPoints.every(bp => typeof bp === 'string')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensures data range is fully covered', () => {
|
||||||
|
// Test various data ranges to ensure min/max are always included
|
||||||
|
const testCases = [
|
||||||
|
{ data: [0, 100], buckets: 5 },
|
||||||
|
{ data: [0.1, 99.9], buckets: 4 },
|
||||||
|
{ data: [-50, 50], buckets: 10 },
|
||||||
|
{ data: [3.2, 38.7], buckets: 5 }, // Original max bug case
|
||||||
|
{ data: [3.14, 100], buckets: 5 }, // Min rounding bug case (3.14 -> 3)
|
||||||
|
{ data: [2.345, 10], buckets: 4 }, // Min rounding bug case (2.345 -> 2.35)
|
||||||
|
{ data: [0.0001, 0.0009], buckets: 3 }, // Very small numbers
|
||||||
|
{ data: [1000000, 9000000], buckets: 8 }, // Large numbers
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ data, buckets }) => {
|
||||||
|
const [min, max] = data;
|
||||||
|
const features = [{ value: min }, { value: max }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: String(buckets) },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
// Critical: min and max must be within the breakpoint range
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(min);
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(max);
|
||||||
|
expect(breakPoints).toHaveLength(buckets + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles uniform distribution correctly', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: 0 },
|
||||||
|
{ value: 25 },
|
||||||
|
{ value: 50 },
|
||||||
|
{ value: 75 },
|
||||||
|
{ value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '4' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that breakpoints are evenly spaced
|
||||||
|
const numericBreakPoints = breakPoints.map(parseFloat);
|
||||||
|
const deltas = [];
|
||||||
|
for (let i = 1; i < numericBreakPoints.length; i += 1) {
|
||||||
|
deltas.push(numericBreakPoints[i] - numericBreakPoints[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All deltas should be approximately equal
|
||||||
|
const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
|
||||||
|
deltas.forEach(delta => {
|
||||||
|
expect(delta).toBeCloseTo(avgDelta, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles single value datasets', () => {
|
||||||
|
const features = [{ value: 42 }, { value: 42 }, { value: 42 }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(42);
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves appropriate precision for different scales', () => {
|
||||||
|
const testCases = [
|
||||||
|
{ data: [0, 1], expectedMaxPrecision: 1 }, // 0.0, 0.2, 0.4...
|
||||||
|
{ data: [0, 0.1], expectedMaxPrecision: 2 }, // 0.00, 0.02...
|
||||||
|
{ data: [0, 0.01], expectedMaxPrecision: 3 }, // 0.000, 0.002...
|
||||||
|
{ data: [0, 1000], expectedMaxPrecision: 0 }, // 0, 200, 400...
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ data, expectedMaxPrecision }) => {
|
||||||
|
const [min, max] = data;
|
||||||
|
const features = [{ value: min }, { value: max }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
breakPoints.forEach(bp => {
|
||||||
|
const decimalPlaces = (bp.split('.')[1] || '').length;
|
||||||
|
expect(decimalPlaces).toBeLessThanOrEqual(expectedMaxPrecision);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles negative values correctly', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: -100 },
|
||||||
|
{ value: -50 },
|
||||||
|
{ value: 0 },
|
||||||
|
{ value: 50 },
|
||||||
|
{ value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const numericBreakPoints = breakPoints.map(parseFloat);
|
||||||
|
expect(numericBreakPoints[0]).toBeLessThanOrEqual(-100);
|
||||||
|
expect(
|
||||||
|
numericBreakPoints[numericBreakPoints.length - 1],
|
||||||
|
).toBeGreaterThanOrEqual(100);
|
||||||
|
|
||||||
|
// Verify ascending order
|
||||||
|
for (let i = 1; i < numericBreakPoints.length; i += 1) {
|
||||||
|
expect(numericBreakPoints[i]).toBeGreaterThan(
|
||||||
|
numericBreakPoints[i - 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles mixed integer and decimal values', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: 1 },
|
||||||
|
{ value: 2.5 },
|
||||||
|
{ value: 3.7 },
|
||||||
|
{ value: 5 },
|
||||||
|
{ value: 8.2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '4' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(1);
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(8.2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses floor/ceil for boundary breakpoints to ensure inclusion', () => {
|
||||||
|
// Test that Math.floor and Math.ceil are used for boundaries
|
||||||
|
// This ensures all data points fall within the breakpoint range
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ minValue: 3.14, maxValue: 100, buckets: 5 },
|
||||||
|
{ minValue: 2.345, maxValue: 10.678, buckets: 4 },
|
||||||
|
{ minValue: 1.67, maxValue: 5.33, buckets: 3 },
|
||||||
|
{ minValue: 0.123, maxValue: 0.987, buckets: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ minValue, maxValue, buckets }) => {
|
||||||
|
const features = [{ value: minValue }, { value: maxValue }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: String(buckets) },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
// First breakpoint should be floored (always <= minValue)
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(minValue);
|
||||||
|
|
||||||
|
// Last breakpoint should be ceiled (always >= maxValue)
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
|
||||||
|
|
||||||
|
// All values should be within range
|
||||||
|
expect(minValue).toBeGreaterThanOrEqual(firstBp);
|
||||||
|
expect(maxValue).toBeLessThanOrEqual(lastBp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents minimum value exclusion edge case', () => {
|
||||||
|
// Specific edge case test for minimum value exclusion
|
||||||
|
// Tests the exact scenario where rounding would exclude the min value
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ value: 3.14 }, // This would round to 3 at precision 0
|
||||||
|
{ value: 50 },
|
||||||
|
{ value: 100 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
|
||||||
|
// The first breakpoint must be <= 3.14 (floor behavior)
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(3.14);
|
||||||
|
|
||||||
|
// Verify that 3.14 is not excluded
|
||||||
|
expect(3.14).toBeGreaterThanOrEqual(firstBp);
|
||||||
|
|
||||||
|
// The first breakpoint should be a clean floor value
|
||||||
|
expect(breakPoints[0]).toMatch(/^3(\.0*)?$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents maximum value exclusion edge case', () => {
|
||||||
|
// Specific edge case test for maximum value exclusion
|
||||||
|
// Tests the exact scenario where rounding would exclude the max value
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ value: 0 },
|
||||||
|
{ value: 20 },
|
||||||
|
{ value: 38.7 }, // Original bug case
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
// The last breakpoint must be >= 38.7 (ceil behavior)
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(38.7);
|
||||||
|
|
||||||
|
// Verify that 38.7 is not excluded
|
||||||
|
expect(38.7).toBeLessThanOrEqual(lastBp);
|
||||||
|
|
||||||
|
// The last breakpoint should be a clean ceil value
|
||||||
|
expect(breakPoints[breakPoints.length - 1]).toMatch(/^39(\.0*)?$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom breakpoints', () => {
|
||||||
|
it('uses custom breakpoints when provided', () => {
|
||||||
|
const features = [{ value: 5 }, { value: 15 }, { value: 25 }];
|
||||||
|
const customBreakPoints = ['0', '10', '20', '30', '40'];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: customBreakPoints, num_buckets: '' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual(['0', '10', '20', '30', '40']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts custom breakpoints in ascending order', () => {
|
||||||
|
const features = [{ value: 5 }];
|
||||||
|
const customBreakPoints = ['30', '10', '0', '20'];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: customBreakPoints, num_buckets: '' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual(['0', '10', '20', '30']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores num_buckets when custom breakpoints are provided', () => {
|
||||||
|
const features = [{ value: 5 }];
|
||||||
|
const customBreakPoints = ['0', '50', '100'];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: customBreakPoints, num_buckets: '10' }, // num_buckets should be ignored
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual(['0', '50', '100']);
|
||||||
|
expect(breakPoints).toHaveLength(3); // not 11
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases and error handling', () => {
|
||||||
|
it('returns empty array when features are undefined', () => {
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
undefined as any,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when features is null', () => {
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
null as any,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when all values are undefined', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: undefined },
|
||||||
|
{ value: undefined },
|
||||||
|
{ value: undefined },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty features array', () => {
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
[],
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(breakPoints).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles string values that can be parsed as numbers', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: '10.5' },
|
||||||
|
{ value: '20.3' },
|
||||||
|
{ value: '30.7' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '3' },
|
||||||
|
features,
|
||||||
|
(d: any) =>
|
||||||
|
typeof d.value === 'string' ? parseFloat(d.value) : d.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(10.5);
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(30.7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default number of buckets when not specified', () => {
|
||||||
|
const features = [{ value: 0 }, { value: 100 }];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should use DEFAULT_NUM_BUCKETS (10)
|
||||||
|
expect(breakPoints).toHaveLength(11); // 10 buckets = 11 breakpoints
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles Infinity and -Infinity values', () => {
|
||||||
|
const features = [
|
||||||
|
{ value: -Infinity },
|
||||||
|
{ value: 0 },
|
||||||
|
{ value: Infinity },
|
||||||
|
];
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should return empty array when Infinity values are present
|
||||||
|
expect(breakPoints).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('breakpoint boundaries validation', () => {
|
||||||
|
it('ensures no data points fall outside breakpoint range', () => {
|
||||||
|
// Generate random test data
|
||||||
|
const generateRandomData = (count: number, min: number, max: number) => {
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
data.push({ value: Math.random() * (max - min) + min });
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test with various random datasets
|
||||||
|
for (let i = 0; i < 10; i += 1) {
|
||||||
|
const features = generateRandomData(20, -1000, 1000);
|
||||||
|
const minValue = Math.min(...features.map(f => f.value));
|
||||||
|
const maxValue = Math.max(...features.map(f => f.value));
|
||||||
|
|
||||||
|
const breakPoints = getBreakPoints(
|
||||||
|
{ break_points: [], num_buckets: '5' },
|
||||||
|
features,
|
||||||
|
accessor,
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstBp = parseFloat(breakPoints[0]);
|
||||||
|
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
|
||||||
|
|
||||||
|
// Every data point should fall within the breakpoint range
|
||||||
|
features.forEach(feature => {
|
||||||
|
expect(feature.value).toBeGreaterThanOrEqual(firstBp);
|
||||||
|
expect(feature.value).toBeLessThanOrEqual(lastBp);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The range should be as tight as possible while including all data
|
||||||
|
expect(firstBp).toBeLessThanOrEqual(minValue);
|
||||||
|
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -75,19 +75,35 @@ export function getBreakPoints(
|
|||||||
if (minValue === undefined || maxValue === undefined) {
|
if (minValue === undefined || maxValue === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
// Handle Infinity values
|
||||||
|
if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const delta = (maxValue - minValue) / numBuckets;
|
const delta = (maxValue - minValue) / numBuckets;
|
||||||
const precision =
|
const precision =
|
||||||
delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta)));
|
delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta)));
|
||||||
const extraBucket =
|
|
||||||
maxValue > parseFloat(maxValue.toFixed(precision)) ? 1 : 0;
|
|
||||||
const startValue =
|
|
||||||
minValue < parseFloat(minValue.toFixed(precision))
|
|
||||||
? minValue - 1
|
|
||||||
: minValue;
|
|
||||||
|
|
||||||
return new Array(numBuckets + 1 + extraBucket)
|
// Generate breakpoints
|
||||||
.fill(0)
|
const breakPoints = new Array(numBuckets + 1).fill(0).map((_, i) => {
|
||||||
.map((_, i) => (startValue + i * delta).toFixed(precision));
|
const value = minValue + i * delta;
|
||||||
|
|
||||||
|
// For the first breakpoint, floor to ensure minimum is included
|
||||||
|
if (i === 0) {
|
||||||
|
const scale = Math.pow(10, precision);
|
||||||
|
return (Math.floor(minValue * scale) / scale).toFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the last breakpoint, ceil to ensure maximum is included
|
||||||
|
if (i === numBuckets) {
|
||||||
|
const scale = Math.pow(10, precision);
|
||||||
|
return (Math.ceil(maxValue * scale) / scale).toFixed(precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For middle breakpoints, use standard rounding
|
||||||
|
return value.toFixed(precision);
|
||||||
|
});
|
||||||
|
|
||||||
|
return breakPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formDataBreakPoints.sort(
|
return formDataBreakPoints.sort(
|
||||||
@@ -146,7 +162,10 @@ export function getBreakPointColorScaler(
|
|||||||
scaler = scaleThreshold<number, string>()
|
scaler = scaleThreshold<number, string>()
|
||||||
.domain(points)
|
.domain(points)
|
||||||
.range(bucketedColors);
|
.range(bucketedColors);
|
||||||
maskPoint = value => !!value && (value > points[n] || value < points[0]);
|
// Only mask values that are strictly outside the min/max bounds
|
||||||
|
// Include values equal to the max breakpoint
|
||||||
|
maskPoint = value =>
|
||||||
|
!!value && (value > points[points.length - 1] || value < points[0]);
|
||||||
} else {
|
} else {
|
||||||
// interpolate colors linearly
|
// interpolate colors linearly
|
||||||
const linearScaleDomain = extent(features, accessor);
|
const linearScaleDomain = extent(features, accessor);
|
||||||
|
|||||||
@@ -322,12 +322,6 @@ export default function transformProps(
|
|||||||
primarySeries.add(seriesOption.id as string);
|
primarySeries.add(seriesOption.id as string);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
rawSeriesA.forEach(seriesOption =>
|
|
||||||
mapSeriesIdToAxis(seriesOption, yAxisIndex),
|
|
||||||
);
|
|
||||||
rawSeriesB.forEach(seriesOption =>
|
|
||||||
mapSeriesIdToAxis(seriesOption, yAxisIndexB),
|
|
||||||
);
|
|
||||||
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
|
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
|
||||||
stack,
|
stack,
|
||||||
onlyTotal,
|
onlyTotal,
|
||||||
@@ -460,7 +454,11 @@ export default function transformProps(
|
|||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (transformedSeries) series.push(transformedSeries);
|
|
||||||
|
if (transformedSeries) {
|
||||||
|
series.push(transformedSeries);
|
||||||
|
mapSeriesIdToAxis(transformedSeries, yAxisIndex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rawSeriesB.forEach(entry => {
|
rawSeriesB.forEach(entry => {
|
||||||
@@ -528,7 +526,11 @@ export default function transformProps(
|
|||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (transformedSeries) series.push(transformedSeries);
|
|
||||||
|
if (transformedSeries) {
|
||||||
|
series.push(transformedSeries);
|
||||||
|
mapSeriesIdToAxis(transformedSeries, yAxisIndexB);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// default to 0-100% range when doing row-level contribution chart
|
// default to 0-100% range when doing row-level contribution chart
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export default {
|
|||||||
dash_edit_perm: true,
|
dash_edit_perm: true,
|
||||||
dash_save_perm: true,
|
dash_save_perm: true,
|
||||||
common: {
|
common: {
|
||||||
flash_messages: [],
|
|
||||||
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
|
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
|
||||||
},
|
},
|
||||||
filterBarOrientation: FilterBarOrientation.Vertical,
|
filterBarOrientation: FilterBarOrientation.Vertical,
|
||||||
|
|||||||
11
superset-frontend/src/.eslintrc.json
Normal file
11
superset-frontend/src/.eslintrc.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.ts", "*.test.tsx", "*.test.js", "*.test.jsx"],
|
||||||
|
"rules": {
|
||||||
|
"jest/consistent-test-it": ["error", {"fn": "test"}],
|
||||||
|
"no-restricted-globals": ["error", "describe", "it"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -338,7 +338,6 @@ export function runQuery(query, runPreviewOnly) {
|
|||||||
const postPayload = {
|
const postPayload = {
|
||||||
client_id: query.id,
|
client_id: query.id,
|
||||||
database_id: query.dbId,
|
database_id: query.dbId,
|
||||||
json: true,
|
|
||||||
runAsync: query.runAsync,
|
runAsync: query.runAsync,
|
||||||
catalog: query.catalog,
|
catalog: query.catalog,
|
||||||
schema: query.schema,
|
schema: query.schema,
|
||||||
@@ -956,7 +955,7 @@ export function addTable(queryEditor, tableName, catalogName, schemaName) {
|
|||||||
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
|
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
|
||||||
const table = {
|
const table = {
|
||||||
dbId,
|
dbId,
|
||||||
queryEditorId: queryEditor.id,
|
queryEditorId: queryEditor.tabViewId ?? queryEditor.id,
|
||||||
catalog: catalogName,
|
catalog: catalogName,
|
||||||
schema: schemaName,
|
schema: schemaName,
|
||||||
name: tableName,
|
name: tableName,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
isFeatureEnabled: jest.fn(),
|
isFeatureEnabled: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('getUpToDateQuery', () => {
|
describe('getUpToDateQuery', () => {
|
||||||
test('should return the up to date query editor state', () => {
|
test('should return the up to date query editor state', () => {
|
||||||
const outOfUpdatedQueryEditor = {
|
const outOfUpdatedQueryEditor = {
|
||||||
@@ -72,6 +73,7 @@ describe('getUpToDateQuery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('async actions', () => {
|
describe('async actions', () => {
|
||||||
const mockBigNumber = '9223372036854775807';
|
const mockBigNumber = '9223372036854775807';
|
||||||
const queryEditor = {
|
const queryEditor = {
|
||||||
@@ -100,6 +102,7 @@ describe('async actions', () => {
|
|||||||
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
|
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
|
||||||
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
|
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('saveQuery', () => {
|
describe('saveQuery', () => {
|
||||||
const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
|
const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
|
||||||
fetchMock.post(saveQueryEndpoint, { results: { json: {} } });
|
fetchMock.post(saveQueryEndpoint, { results: { json: {} } });
|
||||||
@@ -109,7 +112,7 @@ describe('async actions', () => {
|
|||||||
return request(dispatch, () => initialState);
|
return request(dispatch, () => initialState);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('posts to the correct url', () => {
|
test('posts to the correct url', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
@@ -118,7 +121,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('posts the correct query object', () => {
|
test('posts the correct query object', () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
|
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
|
||||||
const call = fetchMock.calls(saveQueryEndpoint)[0];
|
const call = fetchMock.calls(saveQueryEndpoint)[0];
|
||||||
@@ -131,7 +134,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls 3 dispatch actions', () => {
|
test('calls 3 dispatch actions', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -139,7 +142,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls QUERY_EDITOR_SAVED after making a request', () => {
|
test('calls QUERY_EDITOR_SAVED after making a request', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -147,7 +150,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
|
test('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
@@ -163,6 +166,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('formatQuery', () => {
|
describe('formatQuery', () => {
|
||||||
const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
|
const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
|
||||||
const expectedSql = 'SELECT 1';
|
const expectedSql = 'SELECT 1';
|
||||||
@@ -179,6 +183,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('fetchQueryResults', () => {
|
describe('fetchQueryResults', () => {
|
||||||
const makeRequest = () => {
|
const makeRequest = () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
@@ -186,7 +191,7 @@ describe('async actions', () => {
|
|||||||
return request(dispatch, store.getState);
|
return request(dispatch, store.getState);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('makes the fetch request', () => {
|
test('makes the fetch request', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -194,7 +199,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls requestQueryResults', () => {
|
test('calls requestQueryResults', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -202,7 +207,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('parses large number result without losing precision', () =>
|
test.skip('parses large number result without losing precision', () =>
|
||||||
makeRequest().then(() => {
|
makeRequest().then(() => {
|
||||||
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatch.callCount).toBe(2);
|
||||||
@@ -211,7 +216,7 @@ describe('async actions', () => {
|
|||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('calls querySuccess on fetch success', () => {
|
test('calls querySuccess on fetch success', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -226,7 +231,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls queryFailed on fetch error', () => {
|
test('calls queryFailed on fetch error', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
@@ -248,13 +253,14 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('runQuery without query params', () => {
|
describe('runQuery without query params', () => {
|
||||||
const makeRequest = () => {
|
const makeRequest = () => {
|
||||||
const request = actions.runQuery(query);
|
const request = actions.runQuery(query);
|
||||||
return request(dispatch, () => initialState);
|
return request(dispatch, () => initialState);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('makes the fetch request', () => {
|
test('makes the fetch request', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -262,7 +268,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls startQuery', () => {
|
test('calls startQuery', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -270,7 +276,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skip('parses large number result without losing precision', () =>
|
test.skip('parses large number result without losing precision', () =>
|
||||||
makeRequest().then(() => {
|
makeRequest().then(() => {
|
||||||
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatch.callCount).toBe(2);
|
||||||
@@ -279,7 +285,7 @@ describe('async actions', () => {
|
|||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('calls querySuccess on fetch success', () => {
|
test('calls querySuccess on fetch success', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -293,7 +299,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls queryFailed on fetch error and logs the error details', () => {
|
test('calls queryFailed on fetch error and logs the error details', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
@@ -324,6 +330,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('runQuery with query params', () => {
|
describe('runQuery with query params', () => {
|
||||||
const { location } = window;
|
const { location } = window;
|
||||||
|
|
||||||
@@ -342,7 +349,7 @@ describe('async actions', () => {
|
|||||||
return request(dispatch, () => initialState);
|
return request(dispatch, () => initialState);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('makes the fetch request', async () => {
|
test('makes the fetch request', async () => {
|
||||||
const runQueryEndpointWithParams =
|
const runQueryEndpointWithParams =
|
||||||
'glob:*/api/v1/sqllab/execute/?foo=bar';
|
'glob:*/api/v1/sqllab/execute/?foo=bar';
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
@@ -355,8 +362,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('reRunQuery', () => {
|
describe('reRunQuery', () => {
|
||||||
it('creates new query with a new id', () => {
|
test('creates new query with a new id', () => {
|
||||||
const id = 'id';
|
const id = 'id';
|
||||||
const state = {
|
const state = {
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -372,6 +380,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('postStopQuery', () => {
|
describe('postStopQuery', () => {
|
||||||
const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
|
const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
|
||||||
fetchMock.post(stopQueryEndpoint, {});
|
fetchMock.post(stopQueryEndpoint, {});
|
||||||
@@ -385,7 +394,7 @@ describe('async actions', () => {
|
|||||||
return request(dispatch);
|
return request(dispatch);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('makes the fetch request', () => {
|
test('makes the fetch request', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -393,7 +402,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls stopQuery', () => {
|
test('calls stopQuery', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -401,7 +410,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends the correct data', () => {
|
test('sends the correct data', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
return makeRequest().then(() => {
|
return makeRequest().then(() => {
|
||||||
@@ -412,8 +421,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('cloneQueryToNewTab', () => {
|
describe('cloneQueryToNewTab', () => {
|
||||||
it('creates new query editor', () => {
|
test('creates new query editor', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const id = 'id';
|
const id = 'id';
|
||||||
@@ -455,6 +465,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('popSavedQuery', () => {
|
describe('popSavedQuery', () => {
|
||||||
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
|
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -508,7 +519,7 @@ describe('async actions', () => {
|
|||||||
supersetClientGetSpy.mockRestore();
|
supersetClientGetSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls API endpint with correct params', async () => {
|
test('calls API endpint with correct params', async () => {
|
||||||
supersetClientGetSpy.mockResolvedValue({
|
supersetClientGetSpy.mockResolvedValue({
|
||||||
json: { result: mockSavedQueryApiResponse },
|
json: { result: mockSavedQueryApiResponse },
|
||||||
});
|
});
|
||||||
@@ -520,7 +531,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches addQueryEditor with correct params on successful API call', async () => {
|
test('dispatches addQueryEditor with correct params on successful API call', async () => {
|
||||||
supersetClientGetSpy.mockResolvedValue({
|
supersetClientGetSpy.mockResolvedValue({
|
||||||
json: { result: mockSavedQueryApiResponse },
|
json: { result: mockSavedQueryApiResponse },
|
||||||
});
|
});
|
||||||
@@ -547,7 +558,7 @@ describe('async actions', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch addDangerToast on API error', async () => {
|
test('should dispatch addDangerToast on API error', async () => {
|
||||||
supersetClientGetSpy.mockResolvedValue(new Error());
|
supersetClientGetSpy.mockResolvedValue(new Error());
|
||||||
|
|
||||||
await makeRequest(1);
|
await makeRequest(1);
|
||||||
@@ -561,8 +572,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('addQueryEditor', () => {
|
describe('addQueryEditor', () => {
|
||||||
it('creates new query editor', () => {
|
test('creates new query editor', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
@@ -582,8 +594,9 @@ describe('async actions', () => {
|
|||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('addNewQueryEditor', () => {
|
describe('addNewQueryEditor', () => {
|
||||||
it('creates new query editor with new tab name', () => {
|
test('creates new query editor with new tab name', () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -621,7 +634,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('set current query editor', () => {
|
test('set current query editor', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
@@ -636,8 +649,9 @@ describe('async actions', () => {
|
|||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('swithQueryEditor', () => {
|
describe('swithQueryEditor', () => {
|
||||||
it('switch to the next tab editor', () => {
|
test('switch to the next tab editor', () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
{
|
{
|
||||||
@@ -650,7 +664,7 @@ describe('async actions', () => {
|
|||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switch to the first tab editor once it reaches the rightmost tab', () => {
|
test('switch to the first tab editor once it reaches the rightmost tab', () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -673,7 +687,7 @@ describe('async actions', () => {
|
|||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switch to the previous tab editor', () => {
|
test('switch to the previous tab editor', () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -692,7 +706,7 @@ describe('async actions', () => {
|
|||||||
expect(store.getActions()).toEqual(expectedActions);
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switch to the last tab editor once it reaches the leftmost tab', () => {
|
test('switch to the last tab editor once it reaches the leftmost tab', () => {
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -715,6 +729,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('backend sync', () => {
|
describe('backend sync', () => {
|
||||||
const updateTabStateEndpoint = 'glob:*/tabstateview/*';
|
const updateTabStateEndpoint = 'glob:*/tabstateview/*';
|
||||||
fetchMock.put(updateTabStateEndpoint, {});
|
fetchMock.put(updateTabStateEndpoint, {});
|
||||||
@@ -745,8 +760,9 @@ describe('async actions', () => {
|
|||||||
|
|
||||||
afterEach(() => fetchMock.resetHistory());
|
afterEach(() => fetchMock.resetHistory());
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('addQueryEditor', () => {
|
describe('addQueryEditor', () => {
|
||||||
it('creates the tab state in the local storage', () => {
|
test('creates the tab state in the local storage', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -769,8 +785,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('removeQueryEditor', () => {
|
describe('removeQueryEditor', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
@@ -785,8 +802,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetDb', () => {
|
describe('queryEditorSetDb', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const dbId = 42;
|
const dbId = 42;
|
||||||
@@ -803,8 +821,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetCatalog', () => {
|
describe('queryEditorSetCatalog', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const catalog = 'public';
|
const catalog = 'public';
|
||||||
@@ -821,8 +840,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetSchema', () => {
|
describe('queryEditorSetSchema', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const schema = 'schema';
|
const schema = 'schema';
|
||||||
@@ -839,8 +859,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetAutorun', () => {
|
describe('queryEditorSetAutorun', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const autorun = true;
|
const autorun = true;
|
||||||
@@ -857,8 +878,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetTitle', () => {
|
describe('queryEditorSetTitle', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const name = 'name';
|
const name = 'name';
|
||||||
@@ -877,6 +899,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetAndSaveSql', () => {
|
describe('queryEditorSetAndSaveSql', () => {
|
||||||
const sql = 'SELECT * ';
|
const sql = 'SELECT * ';
|
||||||
const expectedActions = [
|
const expectedActions = [
|
||||||
@@ -886,8 +909,9 @@ describe('async actions', () => {
|
|||||||
sql,
|
sql,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('with backend persistence flag on', () => {
|
describe('with backend persistence flag on', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
@@ -904,8 +928,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('with backend persistence flag off', () => {
|
describe('with backend persistence flag off', () => {
|
||||||
it('does not update the tab state in the backend', () => {
|
test('does not update the tab state in the backend', () => {
|
||||||
isFeatureEnabled.mockImplementation(
|
isFeatureEnabled.mockImplementation(
|
||||||
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
|
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
|
||||||
);
|
);
|
||||||
@@ -927,8 +952,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetQueryLimit', () => {
|
describe('queryEditorSetQueryLimit', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const queryLimit = 10;
|
const queryLimit = 10;
|
||||||
@@ -947,8 +973,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('queryEditorSetTemplateParams', () => {
|
describe('queryEditorSetTemplateParams', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const templateParams = '{"foo": "bar"}';
|
const templateParams = '{"foo": "bar"}';
|
||||||
@@ -968,8 +995,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('addTable', () => {
|
describe('addTable', () => {
|
||||||
it('dispatches table state from unsaved change', () => {
|
test('dispatches table state from unsaved change', () => {
|
||||||
const tableName = 'table';
|
const tableName = 'table';
|
||||||
const catalogName = null;
|
const catalogName = null;
|
||||||
const schemaName = 'schema';
|
const schemaName = 'schema';
|
||||||
@@ -1002,10 +1030,90 @@ describe('async actions', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uses tabViewId when available', () => {
|
||||||
|
const tableName = 'table';
|
||||||
|
const catalogName = null;
|
||||||
|
const schemaName = 'schema';
|
||||||
|
const expectedDbId = 473892;
|
||||||
|
const tabViewId = '123';
|
||||||
|
const queryWithTabViewId = { ...query, tabViewId };
|
||||||
|
|
||||||
|
const store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
unsavedQueryEditor: {
|
||||||
|
id: query.id,
|
||||||
|
dbId: expectedDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = actions.addTable(
|
||||||
|
queryWithTabViewId,
|
||||||
|
tableName,
|
||||||
|
catalogName,
|
||||||
|
schemaName,
|
||||||
|
);
|
||||||
|
request(store.dispatch, store.getState);
|
||||||
|
|
||||||
|
expect(store.getActions()[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
table: expect.objectContaining({
|
||||||
|
name: tableName,
|
||||||
|
catalog: catalogName,
|
||||||
|
schema: schemaName,
|
||||||
|
dbId: expectedDbId,
|
||||||
|
queryEditorId: tabViewId, // Should use tabViewId, not id
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to id when tabViewId is not available', () => {
|
||||||
|
const tableName = 'table';
|
||||||
|
const catalogName = null;
|
||||||
|
const schemaName = 'schema';
|
||||||
|
const expectedDbId = 473892;
|
||||||
|
const queryWithoutTabViewId = { ...query, tabViewId: undefined };
|
||||||
|
|
||||||
|
const store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
sqlLab: {
|
||||||
|
...initialState.sqlLab,
|
||||||
|
unsavedQueryEditor: {
|
||||||
|
id: query.id,
|
||||||
|
dbId: expectedDbId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = actions.addTable(
|
||||||
|
queryWithoutTabViewId,
|
||||||
|
tableName,
|
||||||
|
catalogName,
|
||||||
|
schemaName,
|
||||||
|
);
|
||||||
|
request(store.dispatch, store.getState);
|
||||||
|
|
||||||
|
expect(store.getActions()[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
table: expect.objectContaining({
|
||||||
|
name: tableName,
|
||||||
|
catalog: catalogName,
|
||||||
|
schema: schemaName,
|
||||||
|
dbId: expectedDbId,
|
||||||
|
queryEditorId: query.id, // Should use id when tabViewId is not available
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('syncTable', () => {
|
describe('syncTable', () => {
|
||||||
it('updates the table schema state in the backend', () => {
|
test('updates the table schema state in the backend', () => {
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
|
|
||||||
const tableName = 'table';
|
const tableName = 'table';
|
||||||
@@ -1028,6 +1136,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('runTablePreviewQuery', () => {
|
describe('runTablePreviewQuery', () => {
|
||||||
const results = {
|
const results = {
|
||||||
data: mockBigNumber,
|
data: mockBigNumber,
|
||||||
@@ -1058,7 +1167,7 @@ describe('async actions', () => {
|
|||||||
fetchMock.resetHistory();
|
fetchMock.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates and runs data preview query when configured', () => {
|
test('updates and runs data preview query when configured', () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const expectedActionTypes = [
|
const expectedActionTypes = [
|
||||||
@@ -1082,7 +1191,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs data preview query only', () => {
|
test('runs data preview query only', () => {
|
||||||
const expectedActionTypes = [
|
const expectedActionTypes = [
|
||||||
actions.START_QUERY, // runQuery (data preview)
|
actions.START_QUERY, // runQuery (data preview)
|
||||||
actions.QUERY_SUCCESS, // querySuccess
|
actions.QUERY_SUCCESS, // querySuccess
|
||||||
@@ -1107,8 +1216,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('expandTable', () => {
|
describe('expandTable', () => {
|
||||||
it('updates the table schema state in the backend', () => {
|
test('updates the table schema state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const table = { id: 1 };
|
const table = { id: 1 };
|
||||||
@@ -1126,8 +1236,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('collapseTable', () => {
|
describe('collapseTable', () => {
|
||||||
it('updates the table schema state in the backend', () => {
|
test('updates the table schema state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const table = { id: 1 };
|
const table = { id: 1 };
|
||||||
@@ -1145,8 +1256,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('removeTables', () => {
|
describe('removeTables', () => {
|
||||||
it('updates the table schema state in the backend', () => {
|
test('updates the table schema state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const table = { id: 1, initialized: true };
|
const table = { id: 1, initialized: true };
|
||||||
@@ -1163,7 +1275,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes multiple tables and updates the table schema state in the backend', () => {
|
test('deletes multiple tables and updates the table schema state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const tables = [
|
const tables = [
|
||||||
@@ -1183,7 +1295,7 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only updates the initialized table schema state in the backend', () => {
|
test('only updates the initialized table schema state in the backend', () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const tables = [{ id: 1 }, { id: 2, initialized: true }];
|
const tables = [{ id: 1 }, { id: 2, initialized: true }];
|
||||||
@@ -1201,8 +1313,9 @@ describe('async actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('syncQueryEditor', () => {
|
describe('syncQueryEditor', () => {
|
||||||
it('updates the tab state in the backend', () => {
|
test('updates the tab state in the backend', () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
|
|||||||
@@ -68,12 +68,13 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('AceEditorWrapper', () => {
|
describe('AceEditorWrapper', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(FullSQLEditor as any as jest.Mock).mockClear();
|
(FullSQLEditor as any as jest.Mock).mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders ace editor including sql value', async () => {
|
test('renders ace editor including sql value', async () => {
|
||||||
const store = createStore(initialState, reducerIndex);
|
const store = createStore(initialState, reducerIndex);
|
||||||
const { getByTestId } = setup(defaultQueryEditor, store);
|
const { getByTestId } = setup(defaultQueryEditor, store);
|
||||||
await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
|
await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
|
||||||
@@ -83,7 +84,7 @@ describe('AceEditorWrapper', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders current sql for unrelated unsaved changes', () => {
|
test('renders current sql for unrelated unsaved changes', () => {
|
||||||
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
||||||
const store = createStore(
|
const store = createStore(
|
||||||
{
|
{
|
||||||
@@ -108,7 +109,7 @@ describe('AceEditorWrapper', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips rerendering for updating cursor position', () => {
|
test('skips rerendering for updating cursor position', () => {
|
||||||
const store = createStore(initialState, reducerIndex);
|
const store = createStore(initialState, reducerIndex);
|
||||||
setup(defaultQueryEditor, store);
|
setup(defaultQueryEditor, store);
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const AceEditorWrapper = ({
|
|||||||
'catalog',
|
'catalog',
|
||||||
'schema',
|
'schema',
|
||||||
'templateParams',
|
'templateParams',
|
||||||
|
'tabViewId',
|
||||||
]);
|
]);
|
||||||
// Prevent a maximum update depth exceeded error
|
// Prevent a maximum update depth exceeded error
|
||||||
// by skipping access the unsaved query editor state
|
// by skipping access the unsaved query editor state
|
||||||
@@ -172,6 +173,7 @@ const AceEditorWrapper = ({
|
|||||||
dbId: queryEditor.dbId,
|
dbId: queryEditor.dbId,
|
||||||
catalog: queryEditor.catalog,
|
catalog: queryEditor.catalog,
|
||||||
schema: queryEditor.schema,
|
schema: queryEditor.schema,
|
||||||
|
tabViewId: queryEditor.tabViewId,
|
||||||
},
|
},
|
||||||
!autocomplete,
|
!autocomplete,
|
||||||
);
|
);
|
||||||
@@ -191,6 +193,11 @@ const AceEditorWrapper = ({
|
|||||||
width: ${theme.sizeUnit * 130}px !important;
|
width: ${theme.sizeUnit * 130}px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ace_completion-highlight {
|
||||||
|
color: ${theme.colorPrimaryText} !important;
|
||||||
|
background-color: ${theme.colorPrimaryBgHover};
|
||||||
|
}
|
||||||
|
|
||||||
.ace_tooltip {
|
.ace_tooltip {
|
||||||
max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
|
max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type Params = {
|
|||||||
dbId?: string | number;
|
dbId?: string | number;
|
||||||
catalog?: string | null;
|
catalog?: string | null;
|
||||||
schema?: string;
|
schema?: string;
|
||||||
|
tabViewId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EMPTY_LIST = [] as typeof sqlKeywords;
|
const EMPTY_LIST = [] as typeof sqlKeywords;
|
||||||
@@ -59,7 +60,7 @@ const getHelperText = (value: string) =>
|
|||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
|
|
||||||
export function useKeywords(
|
export function useKeywords(
|
||||||
{ queryEditorId, dbId, catalog, schema }: Params,
|
{ queryEditorId, dbId, catalog, schema, tabViewId }: Params,
|
||||||
skip = false,
|
skip = false,
|
||||||
) {
|
) {
|
||||||
const useCustomKeywords = extensionsRegistry.get(
|
const useCustomKeywords = extensionsRegistry.get(
|
||||||
@@ -147,7 +148,12 @@ export function useKeywords(
|
|||||||
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
|
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
|
||||||
if (data.meta === 'table') {
|
if (data.meta === 'table') {
|
||||||
dispatch(
|
dispatch(
|
||||||
addTable({ id: queryEditorId, dbId }, data.value, catalog, schema),
|
addTable(
|
||||||
|
{ id: queryEditorId, dbId, tabViewId },
|
||||||
|
data.value,
|
||||||
|
catalog,
|
||||||
|
schema,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const sqlLabReducer = combineReducers({
|
|||||||
});
|
});
|
||||||
const mockAction = {} as AnyAction;
|
const mockAction = {} as AnyAction;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SqlLab App', () => {
|
describe('SqlLab App', () => {
|
||||||
const middlewares = [thunk];
|
const middlewares = [thunk];
|
||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
@@ -60,23 +61,23 @@ describe('SqlLab App', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is valid', () => {
|
test('is valid', () => {
|
||||||
expect(isValidElement(<App />)).toBe(true);
|
expect(isValidElement(<App />)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render', () => {
|
test('should render', () => {
|
||||||
const { getByTestId } = render(<App />, { useRedux: true, store });
|
const { getByTestId } = render(<App />, { useRedux: true, store });
|
||||||
expect(getByTestId('SqlLabApp')).toBeInTheDocument();
|
expect(getByTestId('SqlLabApp')).toBeInTheDocument();
|
||||||
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument();
|
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reset hotkey events on unmount', () => {
|
test('reset hotkey events on unmount', () => {
|
||||||
const { unmount } = render(<App />, { useRedux: true, store });
|
const { unmount } = render(<App />, { useRedux: true, store });
|
||||||
unmount();
|
unmount();
|
||||||
expect(Mousetrap.reset).toHaveBeenCalled();
|
expect(Mousetrap.reset).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs current usage warning', () => {
|
test('logs current usage warning', () => {
|
||||||
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
|
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
|
||||||
const initialState = {
|
const initialState = {
|
||||||
localStorageUsageInKilobytes,
|
localStorageUsageInKilobytes,
|
||||||
@@ -100,7 +101,7 @@ describe('SqlLab App', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs current local storage usage', async () => {
|
test('logs current local storage usage', async () => {
|
||||||
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
|
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
|
||||||
const storeExceedLocalStorage = mockStore(
|
const storeExceedLocalStorage = mockStore(
|
||||||
sqlLabReducer(
|
sqlLabReducer(
|
||||||
|
|||||||
@@ -21,22 +21,23 @@ import { render } from 'spec/helpers/testing-library';
|
|||||||
import ColumnElement from 'src/SqlLab/components/ColumnElement';
|
import ColumnElement from 'src/SqlLab/components/ColumnElement';
|
||||||
import { mockedActions, table } from 'src/SqlLab/fixtures';
|
import { mockedActions, table } from 'src/SqlLab/fixtures';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ColumnElement', () => {
|
describe('ColumnElement', () => {
|
||||||
const mockedProps = {
|
const mockedProps = {
|
||||||
actions: mockedActions,
|
actions: mockedActions,
|
||||||
column: table.columns[0],
|
column: table.columns[0],
|
||||||
};
|
};
|
||||||
it('is valid with props', () => {
|
test('is valid with props', () => {
|
||||||
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
|
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
|
||||||
});
|
});
|
||||||
it('renders a proper primary key', () => {
|
test('renders a proper primary key', () => {
|
||||||
const { container } = render(<ColumnElement column={table.columns[0]} />);
|
const { container } = render(<ColumnElement column={table.columns[0]} />);
|
||||||
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
|
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
container.querySelector('[data-test="col-name"]')?.firstChild,
|
container.querySelector('[data-test="col-name"]')?.firstChild,
|
||||||
).toHaveTextContent('id');
|
).toHaveTextContent('id');
|
||||||
});
|
});
|
||||||
it('renders a multi-key column', () => {
|
test('renders a multi-key column', () => {
|
||||||
const { container } = render(<ColumnElement column={table.columns[1]} />);
|
const { container } = render(<ColumnElement column={table.columns[1]} />);
|
||||||
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
|
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
|
||||||
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
|
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
|
||||||
@@ -44,7 +45,7 @@ describe('ColumnElement', () => {
|
|||||||
container.querySelector('[data-test="col-name"]')?.firstChild,
|
container.querySelector('[data-test="col-name"]')?.firstChild,
|
||||||
).toHaveTextContent('first_name');
|
).toHaveTextContent('first_name');
|
||||||
});
|
});
|
||||||
it('renders a column with no keys', () => {
|
test('renders a column with no keys', () => {
|
||||||
const { container } = render(<ColumnElement column={table.columns[2]} />);
|
const { container } = render(<ColumnElement column={table.columns[2]} />);
|
||||||
expect(container.querySelector('i')).not.toBeInTheDocument();
|
expect(container.querySelector('i')).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -53,14 +53,15 @@ const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('EstimateQueryCostButton', () => {
|
describe('EstimateQueryCostButton', () => {
|
||||||
it('renders EstimateQueryCostButton', async () => {
|
test('renders EstimateQueryCostButton', async () => {
|
||||||
const { queryByText } = setup({}, mockStore(initialState));
|
const { queryByText } = setup({}, mockStore(initialState));
|
||||||
|
|
||||||
expect(queryByText('Estimate cost')).toBeInTheDocument();
|
expect(queryByText('Estimate cost')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders label for selected query', async () => {
|
test('renders label for selected query', async () => {
|
||||||
const { queryByText } = setup(
|
const { queryByText } = setup(
|
||||||
{ queryEditorId: extraQueryEditor1.id },
|
{ queryEditorId: extraQueryEditor1.id },
|
||||||
mockStore(initialState),
|
mockStore(initialState),
|
||||||
@@ -69,7 +70,7 @@ describe('EstimateQueryCostButton', () => {
|
|||||||
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
|
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders label for selected query from unsaved', async () => {
|
test('renders label for selected query from unsaved', async () => {
|
||||||
const { queryByText } = setup(
|
const { queryByText } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -87,7 +88,7 @@ describe('EstimateQueryCostButton', () => {
|
|||||||
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
|
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders estimation error result', async () => {
|
test('renders estimation error result', async () => {
|
||||||
const { queryByText, getByText } = setup(
|
const { queryByText, getByText } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -109,7 +110,7 @@ describe('EstimateQueryCostButton', () => {
|
|||||||
expect(queryByText('Estimate error')).toBeInTheDocument();
|
expect(queryByText('Estimate error')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders estimation success result', async () => {
|
test('renders estimation success result', async () => {
|
||||||
const { queryByText, getByText } = setup(
|
const { queryByText, getByText } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
|
|||||||
@@ -47,17 +47,18 @@ const setup = (props: Partial<ExploreCtasResultsButtonProps>, store?: Store) =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ExploreCtasResultsButton', () => {
|
describe('ExploreCtasResultsButton', () => {
|
||||||
const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm');
|
const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm');
|
||||||
postFormSpy.mockImplementation(jest.fn());
|
postFormSpy.mockImplementation(jest.fn());
|
||||||
|
|
||||||
it('renders', async () => {
|
test('renders', async () => {
|
||||||
const { queryByText } = setup({}, mockStore(initialState));
|
const { queryByText } = setup({}, mockStore(initialState));
|
||||||
|
|
||||||
expect(queryByText('Explore')).toBeInTheDocument();
|
expect(queryByText('Explore')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('visualize results', async () => {
|
test('visualize results', async () => {
|
||||||
const { getByText } = setup({}, mockStore(initialState));
|
const { getByText } = setup({}, mockStore(initialState));
|
||||||
|
|
||||||
postFormSpy.mockClear();
|
postFormSpy.mockClear();
|
||||||
@@ -75,7 +76,7 @@ describe('ExploreCtasResultsButton', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('visualize results fails', async () => {
|
test('visualize results fails', async () => {
|
||||||
const { getByText } = setup({}, mockStore(initialState));
|
const { getByText } = setup({}, mockStore(initialState));
|
||||||
|
|
||||||
postFormSpy.mockClear();
|
postFormSpy.mockClear();
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ const setup = (
|
|||||||
useRedux: true,
|
useRedux: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ExploreResultsButton', () => {
|
describe('ExploreResultsButton', () => {
|
||||||
it('renders', async () => {
|
test('renders', async () => {
|
||||||
const { queryByText } = setup(jest.fn(), {
|
const { queryByText } = setup(jest.fn(), {
|
||||||
database: { allows_subquery: true },
|
database: { allows_subquery: true },
|
||||||
});
|
});
|
||||||
@@ -41,7 +42,7 @@ describe('ExploreResultsButton', () => {
|
|||||||
expect(screen.getByRole('button', { name: /Create chart/i })).toBeEnabled();
|
expect(screen.getByRole('button', { name: /Create chart/i })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders disabled if subquery not allowed', async () => {
|
test('renders disabled if subquery not allowed', async () => {
|
||||||
const { queryByText } = setup(jest.fn());
|
const { queryByText } = setup(jest.fn());
|
||||||
expect(queryByText('Create chart')).toBeInTheDocument();
|
expect(queryByText('Create chart')).toBeInTheDocument();
|
||||||
// Updated line to match the actual button name that includes the icon
|
// Updated line to match the actual button name that includes the icon
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const mockState = {
|
|||||||
databases: mockDatabases,
|
databases: mockDatabases,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('QueryAutoRefresh', () => {
|
describe('QueryAutoRefresh', () => {
|
||||||
const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery };
|
const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery };
|
||||||
const successfulQueries: QueryDictionary = {
|
const successfulQueries: QueryDictionary = {
|
||||||
@@ -62,15 +63,15 @@ describe('QueryAutoRefresh', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isQueryRunning returns true for valid running query', () => {
|
test('isQueryRunning returns true for valid running query', () => {
|
||||||
expect(isQueryRunning(runningQuery)).toBe(true);
|
expect(isQueryRunning(runningQuery)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isQueryRunning returns false for valid not-running query', () => {
|
test('isQueryRunning returns false for valid not-running query', () => {
|
||||||
expect(isQueryRunning(successfulQuery)).toBe(false);
|
expect(isQueryRunning(successfulQuery)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('isQueryRunning returns false for invalid query', () => {
|
test('isQueryRunning returns false for invalid query', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(isQueryRunning(null)).toBe(false);
|
expect(isQueryRunning(null)).toBe(false);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -81,15 +82,15 @@ describe('QueryAutoRefresh', () => {
|
|||||||
expect(isQueryRunning({ state: { badFormat: true } })).toBe(false);
|
expect(isQueryRunning({ state: { badFormat: true } })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldCheckForQueries is true for valid running query', () => {
|
test('shouldCheckForQueries is true for valid running query', () => {
|
||||||
expect(shouldCheckForQueries(runningQueries)).toBe(true);
|
expect(shouldCheckForQueries(runningQueries)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldCheckForQueries is false for valid completed query', () => {
|
test('shouldCheckForQueries is false for valid completed query', () => {
|
||||||
expect(shouldCheckForQueries(successfulQueries)).toBe(false);
|
expect(shouldCheckForQueries(successfulQueries)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldCheckForQueries is false for invalid inputs', () => {
|
test('shouldCheckForQueries is false for invalid inputs', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expect(shouldCheckForQueries(null)).toBe(false);
|
expect(shouldCheckForQueries(null)).toBe(false);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -109,7 +110,7 @@ describe('QueryAutoRefresh', () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Attempts to refresh when given pending query', async () => {
|
test('Attempts to refresh when given pending query', async () => {
|
||||||
const store = mockStore({ sqlLab: { ...mockState } });
|
const store = mockStore({ sqlLab: { ...mockState } });
|
||||||
|
|
||||||
fetchMock.get(refreshApi, {
|
fetchMock.get(refreshApi, {
|
||||||
@@ -135,7 +136,7 @@ describe('QueryAutoRefresh', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Attempts to clear inactive queries when updated queries are empty', async () => {
|
test('Attempts to clear inactive queries when updated queries are empty', async () => {
|
||||||
const store = mockStore({ sqlLab: { ...mockState } });
|
const store = mockStore({ sqlLab: { ...mockState } });
|
||||||
|
|
||||||
fetchMock.get(refreshApi, { result: [] });
|
fetchMock.get(refreshApi, { result: [] });
|
||||||
@@ -164,7 +165,7 @@ describe('QueryAutoRefresh', () => {
|
|||||||
expect(fetchMock.calls(refreshApi)).toHaveLength(1);
|
expect(fetchMock.calls(refreshApi)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
|
test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
|
||||||
const store = mockStore({ sqlLab: { ...mockState } });
|
const store = mockStore({ sqlLab: { ...mockState } });
|
||||||
|
|
||||||
fetchMock.get(refreshApi, {
|
fetchMock.get(refreshApi, {
|
||||||
@@ -191,7 +192,7 @@ describe('QueryAutoRefresh', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Does NOT Attempt to refresh when given only completed queries', async () => {
|
test('Does NOT Attempt to refresh when given only completed queries', async () => {
|
||||||
const store = mockStore({ sqlLab: { ...mockState } });
|
const store = mockStore({ sqlLab: { ...mockState } });
|
||||||
|
|
||||||
fetchMock.get(refreshApi, {
|
fetchMock.get(refreshApi, {
|
||||||
@@ -219,7 +220,7 @@ describe('QueryAutoRefresh', () => {
|
|||||||
expect(fetchMock.calls(refreshApi)).toHaveLength(0);
|
expect(fetchMock.calls(refreshApi)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs the failed error for async queries', async () => {
|
test('logs the failed error for async queries', async () => {
|
||||||
const store = mockStore({ sqlLab: { ...mockState } });
|
const store = mockStore({ sqlLab: { ...mockState } });
|
||||||
|
|
||||||
fetchMock.get(refreshApi, {
|
fetchMock.get(refreshApi, {
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('QueryLimitSelect', () => {
|
describe('QueryLimitSelect', () => {
|
||||||
it('renders current query limit size', () => {
|
test('renders current query limit size', () => {
|
||||||
const queryLimit = 10;
|
const queryLimit = 10;
|
||||||
const { getByText } = setup(
|
const { getByText } = setup(
|
||||||
{
|
{
|
||||||
@@ -81,12 +82,12 @@ describe('QueryLimitSelect', () => {
|
|||||||
expect(getByText(queryLimit)).toBeInTheDocument();
|
expect(getByText(queryLimit)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders default query limit for initial queryEditor', () => {
|
test('renders default query limit for initial queryEditor', () => {
|
||||||
const { getByText } = setup({}, mockStore(initialState));
|
const { getByText } = setup({}, mockStore(initialState));
|
||||||
expect(getByText(defaultQueryLimit)).toBeInTheDocument();
|
expect(getByText(defaultQueryLimit)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders queryLimit from unsavedQueryEditor', () => {
|
test('renders queryLimit from unsavedQueryEditor', () => {
|
||||||
const queryLimit = 10000;
|
const queryLimit = 10000;
|
||||||
const { getByText } = setup(
|
const { getByText } = setup(
|
||||||
{},
|
{},
|
||||||
@@ -104,7 +105,7 @@ describe('QueryLimitSelect', () => {
|
|||||||
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
|
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders dropdown select', async () => {
|
test('renders dropdown select', async () => {
|
||||||
const { baseElement, getAllByRole, getByRole } = setup(
|
const { baseElement, getAllByRole, getByRole } = setup(
|
||||||
{ maxRow: 50000 },
|
{ maxRow: 50000 },
|
||||||
mockStore(initialState),
|
mockStore(initialState),
|
||||||
@@ -126,7 +127,7 @@ describe('QueryLimitSelect', () => {
|
|||||||
expect(actualLabels).toEqual(expectedLabels);
|
expect(actualLabels).toEqual(expectedLabels);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders dropdown select correctly when maxRow is less than 10', async () => {
|
test('renders dropdown select correctly when maxRow is less than 10', async () => {
|
||||||
const { baseElement, getAllByRole, getByRole } = setup(
|
const { baseElement, getAllByRole, getByRole } = setup(
|
||||||
{ maxRow: 5 },
|
{ maxRow: 5 },
|
||||||
mockStore(initialState),
|
mockStore(initialState),
|
||||||
@@ -146,7 +147,7 @@ describe('QueryLimitSelect', () => {
|
|||||||
expect(actualLabels).toEqual(expectedLabels);
|
expect(actualLabels).toEqual(expectedLabels);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders dropdown select correctly when maxRow is a multiple of 10', async () => {
|
test('renders dropdown select correctly when maxRow is a multiple of 10', async () => {
|
||||||
const { baseElement, getAllByRole, getByRole } = setup(
|
const { baseElement, getAllByRole, getByRole } = setup(
|
||||||
{ maxRow: 10000 },
|
{ maxRow: 10000 },
|
||||||
mockStore(initialState),
|
mockStore(initialState),
|
||||||
@@ -168,7 +169,7 @@ describe('QueryLimitSelect', () => {
|
|||||||
expect(actualLabels).toEqual(expectedLabels);
|
expect(actualLabels).toEqual(expectedLabels);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
|
test('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
|
||||||
const store = mockStore(initialState);
|
const store = mockStore(initialState);
|
||||||
const expectedIndex = 1;
|
const expectedIndex = 1;
|
||||||
const { baseElement, getAllByRole, getByRole } = setup({}, store);
|
const { baseElement, getAllByRole, getByRole } = setup({}, store);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const mockedProps = {
|
|||||||
latestQueryId: 'ryhMUZCGb',
|
latestQueryId: 'ryhMUZCGb',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('QueryTable', () => {
|
describe('QueryTable', () => {
|
||||||
test('is valid', () => {
|
test('is valid', () => {
|
||||||
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);
|
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ const setup = (props?: any, store?: Store) =>
|
|||||||
...(store && { store }),
|
...(store && { store }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ResultSet', () => {
|
describe('ResultSet', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setupAGGridModules();
|
setupAGGridModules();
|
||||||
|
|||||||
@@ -53,17 +53,17 @@ const setup = (props?: Partial<RunQueryActionButtonProps>, store?: Store) =>
|
|||||||
...(store && { store }),
|
...(store && { store }),
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a single Button', () => {
|
test('renders a single Button', () => {
|
||||||
const { getByRole } = setup({}, mockStore(initialState));
|
const { getByRole } = setup({}, mockStore(initialState));
|
||||||
expect(getByRole('button')).toBeInTheDocument();
|
expect(getByRole('button')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a label for Run Query', () => {
|
test('renders a label for Run Query', () => {
|
||||||
const { getByText } = setup({}, mockStore(initialState));
|
const { getByText } = setup({}, mockStore(initialState));
|
||||||
expect(getByText('Run')).toBeInTheDocument();
|
expect(getByText('Run')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a label for Selected Query', () => {
|
test('renders a label for Selected Query', () => {
|
||||||
const { getByText } = setup(
|
const { getByText } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -80,7 +80,7 @@ it('renders a label for Selected Query', () => {
|
|||||||
expect(getByText('Run selection')).toBeInTheDocument();
|
expect(getByText('Run selection')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disable button when sql from unsaved changes is empty', () => {
|
test('disable button when sql from unsaved changes is empty', () => {
|
||||||
const { getByRole } = setup(
|
const { getByRole } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -98,7 +98,7 @@ it('disable button when sql from unsaved changes is empty', () => {
|
|||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disable button when selectedText only contains blank contents', () => {
|
test('disable button when selectedText only contains blank contents', () => {
|
||||||
const { getByRole } = setup(
|
const { getByRole } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -116,7 +116,7 @@ it('disable button when selectedText only contains blank contents', () => {
|
|||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('enable default button for unrelated unsaved changes', () => {
|
test('enable default button for unrelated unsaved changes', () => {
|
||||||
const { getByRole } = setup(
|
const { getByRole } = setup(
|
||||||
{},
|
{},
|
||||||
mockStore({
|
mockStore({
|
||||||
@@ -134,7 +134,7 @@ it('enable default button for unrelated unsaved changes', () => {
|
|||||||
expect(button).toBeEnabled();
|
expect(button).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatch runQuery on click', async () => {
|
test('dispatch runQuery on click', async () => {
|
||||||
const runQuery = jest.fn();
|
const runQuery = jest.fn();
|
||||||
const { getByRole } = setup({ runQuery }, mockStore(initialState));
|
const { getByRole } = setup({ runQuery }, mockStore(initialState));
|
||||||
const button = getByRole('button');
|
const button = getByRole('button');
|
||||||
@@ -143,7 +143,7 @@ it('dispatch runQuery on click', async () => {
|
|||||||
await waitFor(() => expect(runQuery).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(runQuery).toHaveBeenCalledTimes(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatch stopQuery on click while running state', async () => {
|
test('dispatch stopQuery on click while running state', async () => {
|
||||||
const stopQuery = jest.fn();
|
const stopQuery = jest.fn();
|
||||||
const { getByRole } = setup(
|
const { getByRole } = setup(
|
||||||
{ queryState: 'running', stopQuery },
|
{ queryState: 'running', stopQuery },
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const overlayMenu = (
|
|||||||
<Menu items={[{ label: 'Save dataset', key: 'save-dataset' }]} />
|
<Menu items={[{ label: 'Save dataset', key: 'save-dataset' }]} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SaveDatasetActionButton', () => {
|
describe('SaveDatasetActionButton', () => {
|
||||||
test('renders a split save button', async () => {
|
test('renders a split save button', async () => {
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -62,8 +62,9 @@ jest.mock('src/explore/exploreUtils/formData', () => ({
|
|||||||
postFormData: jest.fn(),
|
postFormData: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SaveDatasetModal', () => {
|
describe('SaveDatasetModal', () => {
|
||||||
it('renders a "Save as new" field', () => {
|
test('renders a "Save as new" field', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
const saveRadioBtn = screen.getByRole('radio', {
|
const saveRadioBtn = screen.getByRole('radio', {
|
||||||
@@ -80,7 +81,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
expect(inputFieldText).toBeInTheDocument();
|
expect(inputFieldText).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an "Overwrite existing" field', () => {
|
test('renders an "Overwrite existing" field', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
const overwriteRadioBtn = screen.getByRole('radio', {
|
const overwriteRadioBtn = screen.getByRole('radio', {
|
||||||
@@ -96,20 +97,20 @@ describe('SaveDatasetModal', () => {
|
|||||||
expect(placeholderText).toBeInTheDocument();
|
expect(placeholderText).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a close button', () => {
|
test('renders a close button', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a save button when "Save as new" is selected', () => {
|
test('renders a save button when "Save as new" is selected', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
// "Save as new" is selected when the modal opens by default
|
// "Save as new" is selected when the modal opens by default
|
||||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an overwrite button when "Overwrite existing" is selected', () => {
|
test('renders an overwrite button when "Overwrite existing" is selected', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
|
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
|
||||||
@@ -123,7 +124,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the overwrite button as disabled until an existing dataset is selected', async () => {
|
test('renders the overwrite button as disabled until an existing dataset is selected', async () => {
|
||||||
useSelectorMock.mockReturnValue({ ...user });
|
useSelectorMock.mockReturnValue({ ...user });
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
expect(overwriteConfirmationBtn).toBeEnabled();
|
expect(overwriteConfirmationBtn).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a confirm overwrite screen when overwrite is clicked', async () => {
|
test('renders a confirm overwrite screen when overwrite is clicked', async () => {
|
||||||
useSelectorMock.mockReturnValue({ ...user });
|
useSelectorMock.mockReturnValue({ ...user });
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends the schema when creating the dataset', async () => {
|
test('sends the schema when creating the dataset', async () => {
|
||||||
const dummyDispatch = jest.fn().mockResolvedValue({});
|
const dummyDispatch = jest.fn().mockResolvedValue({});
|
||||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
useSelectorMock.mockReturnValue({ ...user });
|
useSelectorMock.mockReturnValue({ ...user });
|
||||||
@@ -221,7 +222,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends the catalog when creating the dataset', async () => {
|
test('sends the catalog when creating the dataset', async () => {
|
||||||
const dummyDispatch = jest.fn().mockResolvedValue({});
|
const dummyDispatch = jest.fn().mockResolvedValue({});
|
||||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||||
useSelectorMock.mockReturnValue({ ...user });
|
useSelectorMock.mockReturnValue({ ...user });
|
||||||
@@ -252,12 +253,12 @@ describe('SaveDatasetModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not renders a checkbox button when template processing is disabled', () => {
|
test('does not renders a checkbox button when template processing is disabled', () => {
|
||||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||||
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a checkbox button when template processing is enabled', () => {
|
test('renders a checkbox button when template processing is enabled', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.EnableTemplateProcessing]: true,
|
[FeatureFlag.EnableTemplateProcessing]: true,
|
||||||
@@ -266,7 +267,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly includes template parameters when template processing is enabled', () => {
|
test('correctly includes template parameters when template processing is enabled', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.EnableTemplateProcessing]: true,
|
[FeatureFlag.EnableTemplateProcessing]: true,
|
||||||
@@ -301,7 +302,7 @@ describe('SaveDatasetModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly excludes template parameters when template processing is enabled', () => {
|
test('correctly excludes template parameters when template processing is enabled', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.EnableTemplateProcessing]: true,
|
[FeatureFlag.EnableTemplateProcessing]: true,
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ const splitSaveBtnProps = {
|
|||||||
const middlewares = [thunk];
|
const middlewares = [thunk];
|
||||||
const mockStore = configureStore(middlewares);
|
const mockStore = configureStore(middlewares);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SavedQuery', () => {
|
describe('SavedQuery', () => {
|
||||||
it('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
|
test('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
|
||||||
const noRenderProps = {
|
const noRenderProps = {
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
database: {
|
database: {
|
||||||
@@ -84,7 +85,7 @@ describe('SavedQuery', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
|
test('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -95,7 +96,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(saveBtn).toBeVisible();
|
expect(saveBtn).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a save query modal when user clicks save button', () => {
|
test('renders a save query modal when user clicks save button', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -111,7 +112,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(saveQueryModalHeader).toBeInTheDocument();
|
expect(saveQueryModalHeader).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the save query modal UI', () => {
|
test('renders the save query modal UI', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -146,7 +147,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(cancelBtn).toBeInTheDocument();
|
expect(cancelBtn).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a "save as new" and "update" button if query already exists', () => {
|
test('renders a "save as new" and "update" button if query already exists', () => {
|
||||||
render(<SaveQuery {...mockedProps} />, {
|
render(<SaveQuery {...mockedProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore({
|
store: mockStore({
|
||||||
@@ -171,7 +172,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(updateBtn).toBeInTheDocument();
|
expect(updateBtn).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
|
test('renders a split save button when allows_virtual_table_explore is enabled', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -186,7 +187,7 @@ describe('SavedQuery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
test('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -205,7 +206,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(saveDatasetHeader).toBeInTheDocument();
|
expect(saveDatasetHeader).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the save dataset modal UI', async () => {
|
test('renders the save dataset modal UI', async () => {
|
||||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
store: mockStore(mockState),
|
store: mockStore(mockState),
|
||||||
@@ -246,7 +247,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(overwritePlaceholderText).toBeInTheDocument();
|
expect(overwritePlaceholderText).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('modal stays open while save is in progress and closes after completion', async () => {
|
test('modal stays open while save is in progress and closes after completion', async () => {
|
||||||
let resolveSave: () => void;
|
let resolveSave: () => void;
|
||||||
const savePromise = new Promise<void>(resolve => {
|
const savePromise = new Promise<void>(resolve => {
|
||||||
resolveSave = resolve;
|
resolveSave = resolve;
|
||||||
@@ -290,7 +291,7 @@ describe('SavedQuery', () => {
|
|||||||
expect(mockOnSave).toHaveBeenCalledTimes(1);
|
expect(mockOnSave).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles save with a new tab that has no changes', async () => {
|
test('handles save with a new tab that has no changes', async () => {
|
||||||
const mockOnSave = jest.fn().mockResolvedValue(undefined);
|
const mockOnSave = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
// Mock state for a new tab with default SQL
|
// Mock state for a new tab with default SQL
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const unsavedQueryEditor = {
|
|||||||
templateParams: '{ "my_value": "foo" }',
|
templateParams: '{ "my_value": "foo" }',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ShareSqlLabQuery', () => {
|
describe('ShareSqlLabQuery', () => {
|
||||||
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
|
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
|
||||||
const storeQueryMockId = 'ci39c3';
|
const storeQueryMockId = 'ci39c3';
|
||||||
@@ -94,6 +95,7 @@ describe('ShareSqlLabQuery', () => {
|
|||||||
|
|
||||||
afterAll(() => fetchMock.reset());
|
afterAll(() => fetchMock.reset());
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('via permalink api', () => {
|
describe('via permalink api', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
mockedIsFeatureEnabled.mockImplementation(() => true);
|
mockedIsFeatureEnabled.mockImplementation(() => true);
|
||||||
@@ -103,7 +105,7 @@ describe('ShareSqlLabQuery', () => {
|
|||||||
mockedIsFeatureEnabled.mockReset();
|
mockedIsFeatureEnabled.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls storeQuery() with the query when getCopyUrl() is called', async () => {
|
test('calls storeQuery() with the query when getCopyUrl() is called', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<ShareSqlLabQuery {...defaultProps} />, {
|
render(<ShareSqlLabQuery {...defaultProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
@@ -121,7 +123,7 @@ describe('ShareSqlLabQuery', () => {
|
|||||||
).toEqual(expected);
|
).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls storeQuery() with unsaved changes', async () => {
|
test('calls storeQuery() with unsaved changes', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<ShareSqlLabQuery {...defaultProps} />, {
|
render(<ShareSqlLabQuery {...defaultProps} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ const createStore = (initState: object) =>
|
|||||||
getDefaultMiddleware().concat(api.middleware, logAction),
|
getDefaultMiddleware().concat(api.middleware, logAction),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SqlEditor', () => {
|
describe('SqlEditor', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.setTimeout(30000);
|
jest.setTimeout(30000);
|
||||||
@@ -192,7 +193,7 @@ describe('SqlEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render SqlEditor if no db selected', async () => {
|
test('does not render SqlEditor if no db selected', async () => {
|
||||||
const queryEditor = initialState.sqlLab.queryEditors[2];
|
const queryEditor = initialState.sqlLab.queryEditors[2];
|
||||||
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
|
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
|
||||||
expect(
|
expect(
|
||||||
@@ -200,7 +201,7 @@ describe('SqlEditor', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders db unavailable message', async () => {
|
test('renders db unavailable message', async () => {
|
||||||
const queryEditor = initialState.sqlLab.queryEditors[1];
|
const queryEditor = initialState.sqlLab.queryEditors[1];
|
||||||
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
|
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
|
||||||
expect(
|
expect(
|
||||||
@@ -210,7 +211,7 @@ describe('SqlEditor', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render a SqlEditorLeftBar', async () => {
|
test('render a SqlEditorLeftBar', async () => {
|
||||||
const { getByTestId, unmount } = setup(mockedProps, store);
|
const { getByTestId, unmount } = setup(mockedProps, store);
|
||||||
|
|
||||||
await waitFor(
|
await waitFor(
|
||||||
@@ -222,7 +223,7 @@ describe('SqlEditor', () => {
|
|||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
// Update other similar tests with timeouts
|
// Update other similar tests with timeouts
|
||||||
it('render an AceEditorWrapper', async () => {
|
test('render an AceEditorWrapper', async () => {
|
||||||
const { findByTestId, unmount } = setup(mockedProps, store);
|
const { findByTestId, unmount } = setup(mockedProps, store);
|
||||||
|
|
||||||
await waitFor(
|
await waitFor(
|
||||||
@@ -233,7 +234,7 @@ describe('SqlEditor', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
|
test('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
|
||||||
const { findByTestId, queryByTestId } = setup(
|
const { findByTestId, queryByTestId } = setup(
|
||||||
{
|
{
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
@@ -245,7 +246,7 @@ describe('SqlEditor', () => {
|
|||||||
expect(queryByTestId('react-ace')).not.toBeInTheDocument();
|
expect(queryByTestId('react-ace')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('avoids rerendering EditorLeftBar and ResultSet while typing', async () => {
|
test('avoids rerendering EditorLeftBar and ResultSet while typing', async () => {
|
||||||
const { findByTestId } = setup(mockedProps, store);
|
const { findByTestId } = setup(mockedProps, store);
|
||||||
const editor = await findByTestId('react-ace');
|
const editor = await findByTestId('react-ace');
|
||||||
const sql = 'select *';
|
const sql = 'select *';
|
||||||
@@ -260,7 +261,7 @@ describe('SqlEditor', () => {
|
|||||||
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
|
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders sql from unsaved change', async () => {
|
test('renders sql from unsaved change', async () => {
|
||||||
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
|
||||||
store = createStore({
|
store = createStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -293,12 +294,12 @@ describe('SqlEditor', () => {
|
|||||||
expect(editor).toHaveValue(expectedSql);
|
expect(editor).toHaveValue(expectedSql);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render a SouthPane', async () => {
|
test('render a SouthPane', async () => {
|
||||||
const { findByTestId } = setup(mockedProps, store);
|
const { findByTestId } = setup(mockedProps, store);
|
||||||
expect(await findByTestId('mock-result-set')).toBeInTheDocument();
|
expect(await findByTestId('mock-result-set')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs query action with ctas false', async () => {
|
test('runs query action with ctas false', async () => {
|
||||||
store = createStore({
|
store = createStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
sqlLab: {
|
sqlLab: {
|
||||||
@@ -338,7 +339,7 @@ describe('SqlEditor', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render a Limit Dropdown', async () => {
|
test('render a Limit Dropdown', async () => {
|
||||||
const defaultQueryLimit = 101;
|
const defaultQueryLimit = 101;
|
||||||
const updatedProps = { ...mockedProps, defaultQueryLimit };
|
const updatedProps = { ...mockedProps, defaultQueryLimit };
|
||||||
const { findByText } = setup(updatedProps, store);
|
const { findByText } = setup(updatedProps, store);
|
||||||
@@ -346,7 +347,7 @@ describe('SqlEditor', () => {
|
|||||||
expect(await findByText('10 000')).toBeInTheDocument();
|
expect(await findByText('10 000')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an Extension if provided', async () => {
|
test('renders an Extension if provided', async () => {
|
||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
|
|
||||||
extensionsRegistry.set('sqleditor.extension.form', () => (
|
extensionsRegistry.set('sqleditor.extension.form', () => (
|
||||||
@@ -360,6 +361,7 @@ describe('SqlEditor', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('with EstimateQueryCost enabled', () => {
|
describe('with EstimateQueryCost enabled', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockIsFeatureEnabled.mockImplementation(
|
mockIsFeatureEnabled.mockImplementation(
|
||||||
@@ -370,7 +372,7 @@ describe('SqlEditor', () => {
|
|||||||
mockIsFeatureEnabled.mockClear();
|
mockIsFeatureEnabled.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends the catalog and schema to the endpoint', async () => {
|
test('sends the catalog and schema to the endpoint', async () => {
|
||||||
const estimateApi = 'http://localhost/api/v1/sqllab/estimate/';
|
const estimateApi = 'http://localhost/api/v1/sqllab/estimate/';
|
||||||
fetchMock.post(estimateApi, {});
|
fetchMock.post(estimateApi, {});
|
||||||
|
|
||||||
@@ -436,6 +438,7 @@ describe('SqlEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('with SqllabBackendPersistence enabled', () => {
|
describe('with SqllabBackendPersistence enabled', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockIsFeatureEnabled.mockImplementation(
|
mockIsFeatureEnabled.mockImplementation(
|
||||||
@@ -446,7 +449,7 @@ describe('SqlEditor', () => {
|
|||||||
mockIsFeatureEnabled.mockClear();
|
mockIsFeatureEnabled.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render loading state when its Editor is not loaded', async () => {
|
test('should render loading state when its Editor is not loaded', async () => {
|
||||||
const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`;
|
const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`;
|
||||||
fetchMock.post(switchTabApi, {});
|
fetchMock.post(switchTabApi, {});
|
||||||
const { getByTestId } = setup(
|
const { getByTestId } = setup(
|
||||||
|
|||||||
@@ -168,6 +168,9 @@ const StyledToolbar = styled.div`
|
|||||||
|
|
||||||
const StyledSidebar = styled.div`
|
const StyledSidebar = styled.div`
|
||||||
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
|
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSqlEditor = styled.div`
|
const StyledSqlEditor = styled.div`
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import TableElement from '../TableElement';
|
|||||||
|
|
||||||
export interface SqlEditorLeftBarProps {
|
export interface SqlEditorLeftBarProps {
|
||||||
queryEditorId: string;
|
queryEditorId: string;
|
||||||
height?: number;
|
|
||||||
database?: DatabaseObject;
|
database?: DatabaseObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +71,6 @@ const LeftBarStyles = styled.div`
|
|||||||
const SqlEditorLeftBar = ({
|
const SqlEditorLeftBar = ({
|
||||||
database,
|
database,
|
||||||
queryEditorId,
|
queryEditorId,
|
||||||
height = 500,
|
|
||||||
}: SqlEditorLeftBarProps) => {
|
}: SqlEditorLeftBarProps) => {
|
||||||
const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
|
const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
|
||||||
({ sqlLab }) =>
|
({ sqlLab }) =>
|
||||||
@@ -84,6 +82,7 @@ const SqlEditorLeftBar = ({
|
|||||||
'dbId',
|
'dbId',
|
||||||
'catalog',
|
'catalog',
|
||||||
'schema',
|
'schema',
|
||||||
|
'tabViewId',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
|
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
|
||||||
@@ -170,7 +169,6 @@ const SqlEditorLeftBar = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowReset = window.location.search === '?reset=1';
|
const shouldShowReset = window.location.search === '?reset=1';
|
||||||
const tableMetaDataHeight = height - 130; // 130 is the height of the selects above
|
|
||||||
|
|
||||||
const handleCatalogChange = useCallback(
|
const handleCatalogChange = useCallback(
|
||||||
(catalog: string | null) => {
|
(catalog: string | null) => {
|
||||||
@@ -227,22 +225,16 @@ const SqlEditorLeftBar = ({
|
|||||||
/>
|
/>
|
||||||
<div className="divider" />
|
<div className="divider" />
|
||||||
<StyledScrollbarContainer>
|
<StyledScrollbarContainer>
|
||||||
<div
|
{tables.map(table => (
|
||||||
css={css`
|
<TableElement
|
||||||
height: ${tableMetaDataHeight}px;
|
table={table}
|
||||||
`}
|
key={table.id}
|
||||||
>
|
activeKey={tables
|
||||||
{tables.map(table => (
|
.filter(({ expanded }) => expanded)
|
||||||
<TableElement
|
.map(({ id }) => id)}
|
||||||
table={table}
|
onChange={onToggleTable}
|
||||||
key={table.id}
|
/>
|
||||||
activeKey={tables
|
))}
|
||||||
.filter(({ expanded }) => expanded)
|
|
||||||
.map(({ id }) => id)}
|
|
||||||
onChange={onToggleTable}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</StyledScrollbarContainer>
|
</StyledScrollbarContainer>
|
||||||
{shouldShowReset && (
|
{shouldShowReset && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -56,15 +56,16 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
|
|||||||
...(store && { store }),
|
...(store && { store }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SqlEditorTabHeader', () => {
|
describe('SqlEditorTabHeader', () => {
|
||||||
it('renders name', () => {
|
test('renders name', () => {
|
||||||
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
|
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
|
||||||
expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument();
|
expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument();
|
||||||
expect(queryByText(extraQueryEditor1.name)).not.toBeInTheDocument();
|
expect(queryByText(extraQueryEditor1.name)).not.toBeInTheDocument();
|
||||||
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders name from unsaved changes', () => {
|
test('renders name from unsaved changes', () => {
|
||||||
const expectedTitle = 'updated title';
|
const expectedTitle = 'updated title';
|
||||||
const { queryByText } = setup(
|
const { queryByText } = setup(
|
||||||
defaultQueryEditor,
|
defaultQueryEditor,
|
||||||
@@ -85,7 +86,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders current name for unrelated unsaved changes', () => {
|
test('renders current name for unrelated unsaved changes', () => {
|
||||||
const unrelatedTitle = 'updated title';
|
const unrelatedTitle = 'updated title';
|
||||||
const { queryByText } = setup(
|
const { queryByText } = setup(
|
||||||
defaultQueryEditor,
|
defaultQueryEditor,
|
||||||
@@ -106,6 +107,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('with dropdown menus', () => {
|
describe('with dropdown menus', () => {
|
||||||
let store = mockStore();
|
let store = mockStore();
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -116,7 +118,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
userEvent.click(dropdown);
|
userEvent.click(dropdown);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch removeQueryEditor action', async () => {
|
test('should dispatch removeQueryEditor action', async () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
@@ -132,7 +134,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch queryEditorSetTitle action', async () => {
|
test('should dispatch queryEditorSetTitle action', async () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
@@ -155,7 +157,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
mockPrompt.mockClear();
|
mockPrompt.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch toggleLeftBar action', async () => {
|
test('should dispatch toggleLeftBar action', async () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
@@ -173,7 +175,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch removeAllOtherQueryEditors action', async () => {
|
test('should dispatch removeAllOtherQueryEditors action', async () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
@@ -194,7 +196,7 @@ describe('SqlEditorTabHeader', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch cloneQueryToNewTab action', async () => {
|
test('should dispatch cloneQueryToNewTab action', async () => {
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ afterEach(() => {
|
|||||||
pathStub.mockReset();
|
pathStub.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('componentDidMount', () => {
|
describe('componentDidMount', () => {
|
||||||
let uriStub = jest.spyOn(URI.prototype, 'search');
|
let uriStub = jest.spyOn(URI.prototype, 'search');
|
||||||
let replaceState = jest.spyOn(window.history, 'replaceState');
|
let replaceState = jest.spyOn(window.history, 'replaceState');
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ test('renders preview', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('table actions', () => {
|
describe('table actions', () => {
|
||||||
test('refreshes table metadata when triggered', async () => {
|
test('refreshes table metadata when triggered', async () => {
|
||||||
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {
|
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {
|
||||||
|
|||||||
@@ -64,13 +64,14 @@ const setup = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('TemplateParamsEditor', () => {
|
describe('TemplateParamsEditor', () => {
|
||||||
it('should render with a title', () => {
|
test('should render with a title', () => {
|
||||||
const { container } = setup();
|
const { container } = setup();
|
||||||
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
|
expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open a modal with the ace editor', async () => {
|
test('should open a modal with the ace editor', async () => {
|
||||||
const { container, getByTestId } = setup();
|
const { container, getByTestId } = setup();
|
||||||
fireEvent.click(getByText(container, 'Parameters'));
|
fireEvent.click(getByText(container, 'Parameters'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -78,7 +79,7 @@ describe('TemplateParamsEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders templateParams', async () => {
|
test('renders templateParams', async () => {
|
||||||
const { container, getByTestId } = setup();
|
const { container, getByTestId } = setup();
|
||||||
fireEvent.click(getByText(container, 'Parameters'));
|
fireEvent.click(getByText(container, 'Parameters'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -89,7 +90,7 @@ describe('TemplateParamsEditor', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders code from unsaved changes', async () => {
|
test('renders code from unsaved changes', async () => {
|
||||||
const expectedCode = 'custom code value';
|
const expectedCode = 'custom code value';
|
||||||
const { container, getByTestId } = setup(
|
const { container, getByTestId } = setup(
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -52,23 +52,25 @@ const apiDataWithTabState = {
|
|||||||
latest_query: null,
|
latest_query: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('getInitialState', () => {
|
describe('getInitialState', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should output the user that is passed in', () => {
|
test('should output the user that is passed in', () => {
|
||||||
expect(getInitialState(apiData).user?.userId).toEqual(1);
|
expect(getInitialState(apiData).user?.userId).toEqual(1);
|
||||||
});
|
});
|
||||||
it('should return undefined instead of null for templateParams', () => {
|
test('should return undefined instead of null for templateParams', () => {
|
||||||
expect(
|
expect(
|
||||||
getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0]
|
getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0]
|
||||||
?.templateParams,
|
?.templateParams,
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('dedupeTabHistory', () => {
|
describe('dedupeTabHistory', () => {
|
||||||
it('should dedupe the tab history', () => {
|
test('should dedupe the tab history', () => {
|
||||||
[
|
[
|
||||||
{ value: [], expected: [] },
|
{ value: [], expected: [] },
|
||||||
{
|
{
|
||||||
@@ -136,8 +138,9 @@ describe('getInitialState', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('dedupe tables schema', () => {
|
describe('dedupe tables schema', () => {
|
||||||
it('should dedupe the table schema', () => {
|
test('should dedupe the table schema', () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'redux',
|
'redux',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -195,7 +198,7 @@ describe('getInitialState', () => {
|
|||||||
expect(initializedTables.map(({ id }) => id)).toEqual([1, 2, 6]);
|
expect(initializedTables.map(({ id }) => id)).toEqual([1, 2, 6]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse the float dttm value', () => {
|
test('should parse the float dttm value', () => {
|
||||||
const startDttmInStr = '1693433503447.166992';
|
const startDttmInStr = '1693433503447.166992';
|
||||||
const endDttmInStr = '1693433503500.23132';
|
const endDttmInStr = '1693433503500.23132';
|
||||||
|
|
||||||
@@ -249,6 +252,7 @@ describe('getInitialState', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('restore unsaved changes for PERSISTENCE mode', () => {
|
describe('restore unsaved changes for PERSISTENCE mode', () => {
|
||||||
const lastUpdatedTime = Date.now();
|
const lastUpdatedTime = Date.now();
|
||||||
const expectedValue = 'updated editor value';
|
const expectedValue = 'updated editor value';
|
||||||
@@ -284,7 +288,7 @@ describe('getInitialState', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('restore unsaved changes for PERSISTENCE mode', () => {
|
test('restore unsaved changes for PERSISTENCE mode', () => {
|
||||||
const apiDataWithLocalStorage = {
|
const apiDataWithLocalStorage = {
|
||||||
...apiData,
|
...apiData,
|
||||||
active_tab: {
|
active_tab: {
|
||||||
@@ -321,7 +325,7 @@ describe('getInitialState', () => {
|
|||||||
).toEqual(apiDataWithTabState.active_tab.id.toString());
|
).toEqual(apiDataWithTabState.active_tab.id.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skip unsaved changes for expired data', () => {
|
test('skip unsaved changes for expired data', () => {
|
||||||
const apiDataWithLocalStorage = {
|
const apiDataWithLocalStorage = {
|
||||||
...apiData,
|
...apiData,
|
||||||
active_tab: {
|
active_tab: {
|
||||||
@@ -345,7 +349,7 @@ describe('getInitialState', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skip unsaved changes for legacy cache data', () => {
|
test('skip unsaved changes for legacy cache data', () => {
|
||||||
const apiDataWithLocalStorage = {
|
const apiDataWithLocalStorage = {
|
||||||
...apiData,
|
...apiData,
|
||||||
active_tab: {
|
active_tab: {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import { table, initialState as mockState } from '../fixtures';
|
|||||||
|
|
||||||
const initialState = mockState.sqlLab;
|
const initialState = mockState.sqlLab;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('sqlLabReducer', () => {
|
describe('sqlLabReducer', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Query editors actions', () => {
|
describe('Query editors actions', () => {
|
||||||
let newState;
|
let newState;
|
||||||
let defaultQueryEditor;
|
let defaultQueryEditor;
|
||||||
@@ -38,12 +40,12 @@ describe('sqlLabReducer', () => {
|
|||||||
newState = sqlLabReducer(newState, action);
|
newState = sqlLabReducer(newState, action);
|
||||||
qe = newState.queryEditors.find(e => e.id === 'abcd');
|
qe = newState.queryEditors.find(e => e.id === 'abcd');
|
||||||
});
|
});
|
||||||
it('should add a query editor', () => {
|
test('should add a query editor', () => {
|
||||||
expect(newState.queryEditors).toHaveLength(
|
expect(newState.queryEditors).toHaveLength(
|
||||||
initialState.queryEditors.length + 1,
|
initialState.queryEditors.length + 1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should merge the current unsaved changes when adding a query editor', () => {
|
test('should merge the current unsaved changes when adding a query editor', () => {
|
||||||
const expectedTitle = 'new updated title';
|
const expectedTitle = 'new updated title';
|
||||||
const updateAction = {
|
const updateAction = {
|
||||||
type: actions.QUERY_EDITOR_SET_TITLE,
|
type: actions.QUERY_EDITOR_SET_TITLE,
|
||||||
@@ -62,7 +64,7 @@ describe('sqlLabReducer', () => {
|
|||||||
newState.queryEditors[newState.queryEditors.length - 1].id,
|
newState.queryEditors[newState.queryEditors.length - 1].id,
|
||||||
).toEqual('efgh');
|
).toEqual('efgh');
|
||||||
});
|
});
|
||||||
it('should remove a query editor', () => {
|
test('should remove a query editor', () => {
|
||||||
expect(newState.queryEditors).toHaveLength(
|
expect(newState.queryEditors).toHaveLength(
|
||||||
initialState.queryEditors.length + 1,
|
initialState.queryEditors.length + 1,
|
||||||
);
|
);
|
||||||
@@ -75,7 +77,7 @@ describe('sqlLabReducer', () => {
|
|||||||
initialState.queryEditors.length,
|
initialState.queryEditors.length,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should select the latest query editor when tabHistory is empty', () => {
|
test('should select the latest query editor when tabHistory is empty', () => {
|
||||||
const currentQE = newState.queryEditors[0];
|
const currentQE = newState.queryEditors[0];
|
||||||
newState = {
|
newState = {
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -94,7 +96,7 @@ describe('sqlLabReducer', () => {
|
|||||||
);
|
);
|
||||||
expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]);
|
expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]);
|
||||||
});
|
});
|
||||||
it('should remove a query editor including unsaved changes', () => {
|
test('should remove a query editor including unsaved changes', () => {
|
||||||
expect(newState.queryEditors).toHaveLength(
|
expect(newState.queryEditors).toHaveLength(
|
||||||
initialState.queryEditors.length + 1,
|
initialState.queryEditors.length + 1,
|
||||||
);
|
);
|
||||||
@@ -116,7 +118,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.dbId).toBeUndefined();
|
expect(newState.unsavedQueryEditor.dbId).toBeUndefined();
|
||||||
expect(newState.unsavedQueryEditor.id).toBeUndefined();
|
expect(newState.unsavedQueryEditor.id).toBeUndefined();
|
||||||
});
|
});
|
||||||
it('should set q query editor active', () => {
|
test('should set q query editor active', () => {
|
||||||
const expectedTitle = 'new updated title';
|
const expectedTitle = 'new updated title';
|
||||||
const addQueryEditorAction = {
|
const addQueryEditorAction = {
|
||||||
type: actions.ADD_QUERY_EDITOR,
|
type: actions.ADD_QUERY_EDITOR,
|
||||||
@@ -139,7 +141,7 @@ describe('sqlLabReducer', () => {
|
|||||||
);
|
);
|
||||||
expect(newState.queryEditors[1].name).toEqual(expectedTitle);
|
expect(newState.queryEditors[1].name).toEqual(expectedTitle);
|
||||||
});
|
});
|
||||||
it('should not fail while setting DB', () => {
|
test('should not fail while setting DB', () => {
|
||||||
const dbId = 9;
|
const dbId = 9;
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SETDB,
|
type: actions.QUERY_EDITOR_SETDB,
|
||||||
@@ -150,7 +152,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
|
expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not fail while setting schema', () => {
|
test('should not fail while setting schema', () => {
|
||||||
const schema = 'foo';
|
const schema = 'foo';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_SCHEMA,
|
type: actions.QUERY_EDITOR_SET_SCHEMA,
|
||||||
@@ -161,7 +163,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.schema).toBe(schema);
|
expect(newState.unsavedQueryEditor.schema).toBe(schema);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not fail while setting autorun', () => {
|
test('should not fail while setting autorun', () => {
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_AUTORUN,
|
type: actions.QUERY_EDITOR_SET_AUTORUN,
|
||||||
queryEditor: qe,
|
queryEditor: qe,
|
||||||
@@ -173,7 +175,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.autorun).toBe(true);
|
expect(newState.unsavedQueryEditor.autorun).toBe(true);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not fail while setting title', () => {
|
test('should not fail while setting title', () => {
|
||||||
const title = 'Untitled Query 1';
|
const title = 'Untitled Query 1';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_TITLE,
|
type: actions.QUERY_EDITOR_SET_TITLE,
|
||||||
@@ -184,7 +186,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.name).toBe(title);
|
expect(newState.unsavedQueryEditor.name).toBe(title);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not fail while setting Sql', () => {
|
test('should not fail while setting Sql', () => {
|
||||||
const sql = 'SELECT nothing from dev_null';
|
const sql = 'SELECT nothing from dev_null';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_SQL,
|
type: actions.QUERY_EDITOR_SET_SQL,
|
||||||
@@ -195,7 +197,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
expect(newState.unsavedQueryEditor.sql).toBe(sql);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not fail while setting queryLimit', () => {
|
test('should not fail while setting queryLimit', () => {
|
||||||
const queryLimit = 101;
|
const queryLimit = 101;
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
|
type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
|
||||||
@@ -206,7 +208,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
|
expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should set selectedText', () => {
|
test('should set selectedText', () => {
|
||||||
const selectedText = 'TEST';
|
const selectedText = 'TEST';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
|
type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
|
||||||
@@ -218,7 +220,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
|
expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
|
||||||
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
|
||||||
});
|
});
|
||||||
it('should not wiped out unsaved changes while delayed async call intercepted', () => {
|
test('should not wiped out unsaved changes while delayed async call intercepted', () => {
|
||||||
const expectedSql = 'Updated SQL WORKING IN PROGRESS--';
|
const expectedSql = 'Updated SQL WORKING IN PROGRESS--';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.QUERY_EDITOR_SET_SQL,
|
type: actions.QUERY_EDITOR_SET_SQL,
|
||||||
@@ -239,7 +241,7 @@ describe('sqlLabReducer', () => {
|
|||||||
interceptedAction.northPercent,
|
interceptedAction.northPercent,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should migrate query editor by new query editor id', () => {
|
test('should migrate query editor by new query editor id', () => {
|
||||||
const { length } = newState.queryEditors;
|
const { length } = newState.queryEditors;
|
||||||
const index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
|
const index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
|
||||||
const newQueryEditor = {
|
const newQueryEditor = {
|
||||||
@@ -267,7 +269,7 @@ describe('sqlLabReducer', () => {
|
|||||||
newQueryEditor.tabViewId,
|
newQueryEditor.tabViewId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should clear the destroyed query editors', () => {
|
test('should clear the destroyed query editors', () => {
|
||||||
const expectedQEId = '1233289';
|
const expectedQEId = '1233289';
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
|
type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
|
||||||
@@ -285,6 +287,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.destroyedQueryEditors).toEqual({});
|
expect(newState.destroyedQueryEditors).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Tables', () => {
|
describe('Tables', () => {
|
||||||
let newState;
|
let newState;
|
||||||
let newTable;
|
let newTable;
|
||||||
@@ -297,12 +300,12 @@ describe('sqlLabReducer', () => {
|
|||||||
newState = sqlLabReducer(initialState, action);
|
newState = sqlLabReducer(initialState, action);
|
||||||
newTable = newState.tables[0];
|
newTable = newState.tables[0];
|
||||||
});
|
});
|
||||||
it('should add a table', () => {
|
test('should add a table', () => {
|
||||||
// Testing that beforeEach actually added the table
|
// Testing that beforeEach actually added the table
|
||||||
expect(newState.tables).toHaveLength(1);
|
expect(newState.tables).toHaveLength(1);
|
||||||
expect(newState.tables[0].expanded).toBe(true);
|
expect(newState.tables[0].expanded).toBe(true);
|
||||||
});
|
});
|
||||||
it('should merge the table attributes', () => {
|
test('should merge the table attributes', () => {
|
||||||
// Merging the extra attribute
|
// Merging the extra attribute
|
||||||
newTable.extra = true;
|
newTable.extra = true;
|
||||||
const action = {
|
const action = {
|
||||||
@@ -313,7 +316,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.tables).toHaveLength(1);
|
expect(newState.tables).toHaveLength(1);
|
||||||
expect(newState.tables[0].extra).toBe(true);
|
expect(newState.tables[0].extra).toBe(true);
|
||||||
});
|
});
|
||||||
it('should overwrite table ID be ignored when the existing table is already initialized', () => {
|
test('should overwrite table ID be ignored when the existing table is already initialized', () => {
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.MERGE_TABLE,
|
type: actions.MERGE_TABLE,
|
||||||
table: newTable,
|
table: newTable,
|
||||||
@@ -345,7 +348,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.tables).toHaveLength(1);
|
expect(newState.tables).toHaveLength(1);
|
||||||
expect(newState.tables[0].id).toBe(remoteId);
|
expect(newState.tables[0].id).toBe(remoteId);
|
||||||
});
|
});
|
||||||
it('should expand and collapse a table', () => {
|
test('should expand and collapse a table', () => {
|
||||||
const collapseTableAction = {
|
const collapseTableAction = {
|
||||||
type: actions.COLLAPSE_TABLE,
|
type: actions.COLLAPSE_TABLE,
|
||||||
table: newTable,
|
table: newTable,
|
||||||
@@ -359,7 +362,7 @@ describe('sqlLabReducer', () => {
|
|||||||
newState = sqlLabReducer(newState, expandTableAction);
|
newState = sqlLabReducer(newState, expandTableAction);
|
||||||
expect(newState.tables[0].expanded).toBe(true);
|
expect(newState.tables[0].expanded).toBe(true);
|
||||||
});
|
});
|
||||||
it('should remove a table', () => {
|
test('should remove a table', () => {
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.REMOVE_TABLES,
|
type: actions.REMOVE_TABLES,
|
||||||
tables: [newTable],
|
tables: [newTable],
|
||||||
@@ -368,6 +371,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.tables).toHaveLength(0);
|
expect(newState.tables).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Run Query', () => {
|
describe('Run Query', () => {
|
||||||
const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439';
|
const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439';
|
||||||
const CHANGED_ON_TIMESTAMP = 1687765985439;
|
const CHANGED_ON_TIMESTAMP = 1687765985439;
|
||||||
@@ -385,7 +389,7 @@ describe('sqlLabReducer', () => {
|
|||||||
sqlEditorId: 'dfsadfs',
|
sqlEditorId: 'dfsadfs',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
it('should start a query', () => {
|
test('should start a query', () => {
|
||||||
const action = {
|
const action = {
|
||||||
type: actions.START_QUERY,
|
type: actions.START_QUERY,
|
||||||
query: {
|
query: {
|
||||||
@@ -401,7 +405,7 @@ describe('sqlLabReducer', () => {
|
|||||||
newState = sqlLabReducer(newState, action);
|
newState = sqlLabReducer(newState, action);
|
||||||
expect(Object.keys(newState.queries)).toHaveLength(1);
|
expect(Object.keys(newState.queries)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
it('should stop the query', () => {
|
test('should stop the query', () => {
|
||||||
const startQueryAction = {
|
const startQueryAction = {
|
||||||
type: actions.START_QUERY,
|
type: actions.START_QUERY,
|
||||||
query,
|
query,
|
||||||
@@ -415,7 +419,7 @@ describe('sqlLabReducer', () => {
|
|||||||
const q = newState.queries[Object.keys(newState.queries)[0]];
|
const q = newState.queries[Object.keys(newState.queries)[0]];
|
||||||
expect(q.state).toBe('stopped');
|
expect(q.state).toBe('stopped');
|
||||||
});
|
});
|
||||||
it('should remove a query', () => {
|
test('should remove a query', () => {
|
||||||
const startQueryAction = {
|
const startQueryAction = {
|
||||||
type: actions.START_QUERY,
|
type: actions.START_QUERY,
|
||||||
query,
|
query,
|
||||||
@@ -428,7 +432,7 @@ describe('sqlLabReducer', () => {
|
|||||||
newState = sqlLabReducer(newState, removeQueryAction);
|
newState = sqlLabReducer(newState, removeQueryAction);
|
||||||
expect(Object.keys(newState.queries)).toHaveLength(0);
|
expect(Object.keys(newState.queries)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
it('should refresh queries when polling returns new results', () => {
|
test('should refresh queries when polling returns new results', () => {
|
||||||
const startDttmInStr = '1693433503447.166992';
|
const startDttmInStr = '1693433503447.166992';
|
||||||
const endDttmInStr = '1693433503500.23132';
|
const endDttmInStr = '1693433503500.23132';
|
||||||
newState = sqlLabReducer(
|
newState = sqlLabReducer(
|
||||||
@@ -449,7 +453,7 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr));
|
expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr));
|
||||||
expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP);
|
expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP);
|
||||||
});
|
});
|
||||||
it('should skip refreshing queries when polling contains existing results', () => {
|
test('should skip refreshing queries when polling contains existing results', () => {
|
||||||
const completedQuery = {
|
const completedQuery = {
|
||||||
...query,
|
...query,
|
||||||
extra: {
|
extra: {
|
||||||
@@ -478,10 +482,11 @@ describe('sqlLabReducer', () => {
|
|||||||
expect(newState.queries.abcd).toBe(query);
|
expect(newState.queries.abcd).toBe(query);
|
||||||
expect(newState.queries.def).toBe(completedQuery);
|
expect(newState.queries.def).toBe(completedQuery);
|
||||||
});
|
});
|
||||||
it('should refresh queries when polling returns empty', () => {
|
test('should refresh queries when polling returns empty', () => {
|
||||||
newState = sqlLabReducer(newState, actions.refreshQueries({}));
|
newState = sqlLabReducer(newState, actions.refreshQueries({}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('CLEAR_INACTIVE_QUERIES', () => {
|
describe('CLEAR_INACTIVE_QUERIES', () => {
|
||||||
let newState;
|
let newState;
|
||||||
let query;
|
let query;
|
||||||
@@ -496,7 +501,7 @@ describe('sqlLabReducer', () => {
|
|||||||
cached: false,
|
cached: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
it('updates queries that have already been completed', () => {
|
test('updates queries that have already been completed', () => {
|
||||||
newState = sqlLabReducer(
|
newState = sqlLabReducer(
|
||||||
{
|
{
|
||||||
...newState,
|
...newState,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from 'src/SqlLab/constants';
|
} from 'src/SqlLab/constants';
|
||||||
import { queries, defaultQueryEditor } from '../fixtures';
|
import { queries, defaultQueryEditor } from '../fixtures';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('reduxStateToLocalStorageHelper', () => {
|
describe('reduxStateToLocalStorageHelper', () => {
|
||||||
const queriesObj: Record<string, any> = {};
|
const queriesObj: Record<string, any> = {};
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -37,7 +38,7 @@ describe('reduxStateToLocalStorageHelper', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => {
|
test('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => {
|
||||||
// make sure sample data contains old query
|
// make sure sample data contains old query
|
||||||
const oldQuery = queries[0];
|
const oldQuery = queries[0];
|
||||||
const { id, startDttm } = oldQuery;
|
const { id, startDttm } = oldQuery;
|
||||||
@@ -51,7 +52,7 @@ describe('reduxStateToLocalStorageHelper', () => {
|
|||||||
expect(emptiedQuery[id].results).toEqual({});
|
expect(emptiedQuery[id].results).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should empty query.results if query,.results size is greater than LOCALSTORAGE_MAX_QUERY_RESULTS_KB', () => {
|
test('should empty query.results if query,.results size is greater than LOCALSTORAGE_MAX_QUERY_RESULTS_KB', () => {
|
||||||
const reasonableSizeQuery = {
|
const reasonableSizeQuery = {
|
||||||
...queries[0],
|
...queries[0],
|
||||||
startDttm: Date.now(),
|
startDttm: Date.now(),
|
||||||
@@ -83,7 +84,7 @@ describe('reduxStateToLocalStorageHelper', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only return selected keys for query editor', () => {
|
test('should only return selected keys for query editor', () => {
|
||||||
const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }];
|
const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }];
|
||||||
expect(Object.keys(queryEditors[0])).toContain('dummy');
|
expect(Object.keys(queryEditors[0])).toContain('dummy');
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ const emptyEditor = {
|
|||||||
remoteId: null,
|
remoteId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('newQueryTabName', () => {
|
describe('newQueryTabName', () => {
|
||||||
it("should return default title if queryEditor's length is 0", () => {
|
test("should return default title if queryEditor's length is 0", () => {
|
||||||
const defaultTitle = 'default title';
|
const defaultTitle = 'default title';
|
||||||
const title = newQueryTabName([], defaultTitle);
|
const title = newQueryTabName([], defaultTitle);
|
||||||
expect(title).toEqual(defaultTitle);
|
expect(title).toEqual(defaultTitle);
|
||||||
});
|
});
|
||||||
it('should return next available number if there are unsaved editors', () => {
|
test('should return next available number if there are unsaved editors', () => {
|
||||||
const untitledQueryText = 'Untitled Query';
|
const untitledQueryText = 'Untitled Query';
|
||||||
const unsavedEditors = [
|
const unsavedEditors = [
|
||||||
{ ...emptyEditor, name: `${untitledQueryText} 1` },
|
{ ...emptyEditor, name: `${untitledQueryText} 1` },
|
||||||
|
|||||||
@@ -20,38 +20,40 @@
|
|||||||
import { alterForComparison, formatValueHandler, getRowsFromDiffs } from '.';
|
import { alterForComparison, formatValueHandler, getRowsFromDiffs } from '.';
|
||||||
import { RowType } from '../types';
|
import { RowType } from '../types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('alterForComparison', () => {
|
describe('alterForComparison', () => {
|
||||||
it('returns null for undefined value', () => {
|
test('returns null for undefined value', () => {
|
||||||
expect(alterForComparison(undefined)).toBeNull();
|
expect(alterForComparison(undefined)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for null value', () => {
|
test('returns null for null value', () => {
|
||||||
expect(alterForComparison(null)).toBeNull();
|
expect(alterForComparison(null)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for empty string value', () => {
|
test('returns null for empty string value', () => {
|
||||||
expect(alterForComparison('')).toBeNull();
|
expect(alterForComparison('')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for empty array value', () => {
|
test('returns null for empty array value', () => {
|
||||||
expect(alterForComparison([])).toBeNull();
|
expect(alterForComparison([])).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for empty object value', () => {
|
test('returns null for empty object value', () => {
|
||||||
expect(alterForComparison({})).toBeNull();
|
expect(alterForComparison({})).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns value for non-empty array', () => {
|
test('returns value for non-empty array', () => {
|
||||||
const value = [1, 2, 3];
|
const value = [1, 2, 3];
|
||||||
expect(alterForComparison(value)).toEqual(value);
|
expect(alterForComparison(value)).toEqual(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns value for non-empty object', () => {
|
test('returns value for non-empty object', () => {
|
||||||
const value = { key: 'value' };
|
const value = { key: 'value' };
|
||||||
expect(alterForComparison(value)).toEqual(value);
|
expect(alterForComparison(value)).toEqual(value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('formatValueHandler', () => {
|
describe('formatValueHandler', () => {
|
||||||
const controlsMap = {
|
const controlsMap = {
|
||||||
b: { type: 'BoundsControl', label: 'Bounds' },
|
b: { type: 'BoundsControl', label: 'Bounds' },
|
||||||
@@ -61,7 +63,7 @@ describe('formatValueHandler', () => {
|
|||||||
other_control: { type: 'OtherControl', label: 'Other' },
|
other_control: { type: 'OtherControl', label: 'Other' },
|
||||||
};
|
};
|
||||||
|
|
||||||
it('handles undefined value', () => {
|
test('handles undefined value', () => {
|
||||||
const value = undefined;
|
const value = undefined;
|
||||||
const key = 'b';
|
const key = 'b';
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue).toBe('N/A');
|
expect(formattedValue).toBe('N/A');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles null value', () => {
|
test('handles null value', () => {
|
||||||
const value = null;
|
const value = null;
|
||||||
const key = 'b';
|
const key = 'b';
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue).toBe('null');
|
expect(formattedValue).toBe('null');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns "[]" for empty filters', () => {
|
test('returns "[]" for empty filters', () => {
|
||||||
const value: unknown[] = [];
|
const value: unknown[] = [];
|
||||||
const key = 'adhoc_filters';
|
const key = 'adhoc_filters';
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue).toBe('[]');
|
expect(formattedValue).toBe('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats filters with array values', () => {
|
test('formats filters with array values', () => {
|
||||||
const filters = [
|
const filters = [
|
||||||
{
|
{
|
||||||
clause: 'WHERE',
|
clause: 'WHERE',
|
||||||
@@ -129,7 +131,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue).toBe(expected);
|
expect(formattedValue).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats filters with string values', () => {
|
test('formats filters with string values', () => {
|
||||||
const filters = [
|
const filters = [
|
||||||
{
|
{
|
||||||
clause: 'WHERE',
|
clause: 'WHERE',
|
||||||
@@ -158,7 +160,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue).toBe(expected);
|
expect(formattedValue).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats "Min" and "Max" for BoundsControl', () => {
|
test('formats "Min" and "Max" for BoundsControl', () => {
|
||||||
const value: number[] = [1, 2];
|
const value: number[] = [1, 2];
|
||||||
const key = 'b';
|
const key = 'b';
|
||||||
|
|
||||||
@@ -167,7 +169,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(result).toEqual('Min: 1, Max: 2');
|
expect(result).toEqual('Min: 1, Max: 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats stringified objects for CollectionControl', () => {
|
test('formats stringified objects for CollectionControl', () => {
|
||||||
const value = [{ a: 1 }, { b: 2 }];
|
const value = [{ a: 1 }, { b: 2 }];
|
||||||
const key = 'column_collection';
|
const key = 'column_collection';
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ describe('formatValueHandler', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats MetricsControl values correctly', () => {
|
test('formats MetricsControl values correctly', () => {
|
||||||
const value = [{ label: 'SUM(Sales)' }, { label: 'Metric2' }];
|
const value = [{ label: 'SUM(Sales)' }, { label: 'Metric2' }];
|
||||||
const key = 'metrics';
|
const key = 'metrics';
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(result).toEqual('SUM(Sales), Metric2');
|
expect(result).toEqual('SUM(Sales), Metric2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats boolean values as string', () => {
|
test('formats boolean values as string', () => {
|
||||||
const value1 = true;
|
const value1 = true;
|
||||||
const value2 = false;
|
const value2 = false;
|
||||||
const key = 'b';
|
const key = 'b';
|
||||||
@@ -207,7 +209,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(formattedValue2).toBe('false');
|
expect(formattedValue2).toBe('false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats array values correctly', () => {
|
test('formats array values correctly', () => {
|
||||||
const value = [
|
const value = [
|
||||||
{ label: 'Label1' },
|
{ label: 'Label1' },
|
||||||
{ label: 'Label2' },
|
{ label: 'Label2' },
|
||||||
@@ -229,7 +231,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats string values correctly', () => {
|
test('formats string values correctly', () => {
|
||||||
const value = 'test';
|
const value = 'test';
|
||||||
const key = 'other_control';
|
const key = 'other_control';
|
||||||
|
|
||||||
@@ -238,7 +240,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(result).toEqual('test');
|
expect(result).toEqual('test');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats number values correctly', () => {
|
test('formats number values correctly', () => {
|
||||||
const value = 123;
|
const value = 123;
|
||||||
const key = 'other_control';
|
const key = 'other_control';
|
||||||
|
|
||||||
@@ -247,7 +249,7 @@ describe('formatValueHandler', () => {
|
|||||||
expect(result).toEqual(123);
|
expect(result).toEqual(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats object values correctly', () => {
|
test('formats object values correctly', () => {
|
||||||
const value = { 1: 2, alpha: 'bravo' };
|
const value = { 1: 2, alpha: 'bravo' };
|
||||||
const key = 'other_control';
|
const key = 'other_control';
|
||||||
const expected = '{"1":2,"alpha":"bravo"}';
|
const expected = '{"1":2,"alpha":"bravo"}';
|
||||||
@@ -258,8 +260,9 @@ describe('formatValueHandler', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('getRowsFromDiffs', () => {
|
describe('getRowsFromDiffs', () => {
|
||||||
it('returns formatted rows for diffs', () => {
|
test('returns formatted rows for diffs', () => {
|
||||||
const diffs = {
|
const diffs = {
|
||||||
metric: { before: [{ label: 'old' }], after: [{ label: 'new' }] },
|
metric: { before: [{ label: 'old' }], after: [{ label: 'new' }] },
|
||||||
limit: { before: 10, after: 20 },
|
limit: { before: 10, after: 20 },
|
||||||
@@ -278,7 +281,7 @@ describe('getRowsFromDiffs', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to key if label is missing', () => {
|
test('falls back to key if label is missing', () => {
|
||||||
const diffs = {
|
const diffs = {
|
||||||
unknown: { before: 'a', after: 'b' },
|
unknown: { before: 'a', after: 'b' },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
Key,
|
|
||||||
ReactNode,
|
ReactNode,
|
||||||
RefObject,
|
RefObject,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -43,18 +42,18 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { RootState } from 'src/dashboard/types';
|
import { RootState } from 'src/dashboard/types';
|
||||||
import { Menu } from '@superset-ui/core/components/Menu';
|
import { MenuItem } from '@superset-ui/core/components/Menu';
|
||||||
import { usePermissions } from 'src/hooks/usePermissions';
|
import { usePermissions } from 'src/hooks/usePermissions';
|
||||||
import { Dropdown } from '@superset-ui/core/components';
|
import { Dropdown } from '@superset-ui/core/components';
|
||||||
import { updateDataMask } from 'src/dataMask/actions';
|
import { updateDataMask } from 'src/dataMask/actions';
|
||||||
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
|
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
|
||||||
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
|
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
|
||||||
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
|
||||||
import { DrillDetailMenuItems } from '../DrillDetail';
|
import { useDrillDetailMenuItems } from '../useDrillDetailMenuItems';
|
||||||
import { getMenuAdjustedY } from '../utils';
|
import { getMenuAdjustedY } from '../utils';
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
import { DrillBySubmenu } from '../DrillBy/DrillBySubmenu';
|
||||||
import { DrillByMenuItems } from '../DrillBy/DrillByMenuItems';
|
|
||||||
import DrillDetailModal from '../DrillDetail/DrillDetailModal';
|
import DrillDetailModal from '../DrillDetail/DrillDetailModal';
|
||||||
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
|
|
||||||
export enum ContextMenuItem {
|
export enum ContextMenuItem {
|
||||||
CrossFilter,
|
CrossFilter,
|
||||||
@@ -94,8 +93,8 @@ const ChartContextMenu = (
|
|||||||
}: ChartContextMenuProps,
|
}: ChartContextMenuProps,
|
||||||
ref: RefObject<ChartContextMenuRef>,
|
ref: RefObject<ChartContextMenuRef>,
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const theme = useTheme();
|
||||||
const { canDrillToDetail, canDrillBy, canDownload } = usePermissions();
|
const { canDrillToDetail, canDrillBy, canDownload } = usePermissions();
|
||||||
|
|
||||||
const crossFiltersEnabled = useSelector<RootState, boolean>(
|
const crossFiltersEnabled = useSelector<RootState, boolean>(
|
||||||
@@ -104,7 +103,6 @@ const ChartContextMenu = (
|
|||||||
const dashboardId = useSelector<RootState, number>(
|
const dashboardId = useSelector<RootState, number>(
|
||||||
({ dashboardInfo }) => dashboardInfo.id,
|
({ dashboardInfo }) => dashboardInfo.id,
|
||||||
);
|
);
|
||||||
const [openKeys, setOpenKeys] = useState<Key[]>([]);
|
|
||||||
|
|
||||||
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
|
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
|
||||||
[],
|
[],
|
||||||
@@ -160,7 +158,6 @@ const ChartContextMenu = (
|
|||||||
|
|
||||||
const closeContextMenu = useCallback(() => {
|
const closeContextMenu = useCallback(() => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setOpenKeys([]);
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
@@ -177,7 +174,7 @@ const ChartContextMenu = (
|
|||||||
setShowDrillByModal(false);
|
setShowDrillByModal(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const menuItems: React.JSX.Element[] = [];
|
const menuItems: MenuItem[] = [];
|
||||||
|
|
||||||
const showDrillToDetail =
|
const showDrillToDetail =
|
||||||
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
|
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
|
||||||
@@ -264,6 +261,20 @@ const ChartContextMenu = (
|
|||||||
itemsCount = 1; // "No actions" appears if no actions in menu
|
itemsCount = 1; // "No actions" appears if no actions in menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const drillDetailMenuItems = useDrillDetailMenuItems({
|
||||||
|
formData: drillFormData,
|
||||||
|
filters: filters?.drillToDetail,
|
||||||
|
setFilters,
|
||||||
|
isContextMenu: true,
|
||||||
|
contextMenuY: clientY,
|
||||||
|
onSelection,
|
||||||
|
submenuIndex: showCrossFilters ? 2 : 1,
|
||||||
|
setShowModal: setDrillModalIsOpen,
|
||||||
|
dataset: filteredDataset,
|
||||||
|
isLoadingDataset,
|
||||||
|
...(additionalConfig?.drillToDetail || {}),
|
||||||
|
});
|
||||||
|
|
||||||
if (showCrossFilters) {
|
if (showCrossFilters) {
|
||||||
const isCrossFilterDisabled =
|
const isCrossFilterDisabled =
|
||||||
!isCrossFilteringSupportedByChart ||
|
!isCrossFilteringSupportedByChart ||
|
||||||
@@ -305,74 +316,65 @@ const ChartContextMenu = (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<>
|
{
|
||||||
<Menu.Item
|
key: 'cross-filtering-menu-item',
|
||||||
key="cross-filtering-menu-item"
|
label: filters?.crossFilter?.isCurrentValueSelected ? (
|
||||||
disabled={isCrossFilterDisabled}
|
t('Remove cross-filter')
|
||||||
onClick={() => {
|
) : (
|
||||||
if (filters?.crossFilter) {
|
<span>
|
||||||
dispatch(updateDataMask(id, filters.crossFilter.dataMask));
|
{t('Add cross-filter')}
|
||||||
}
|
<MenuItemTooltip
|
||||||
}}
|
title={crossFilteringTooltipTitle}
|
||||||
>
|
color={!isCrossFilterDisabled ? theme.colorIcon : undefined}
|
||||||
{filters?.crossFilter?.isCurrentValueSelected ? (
|
/>
|
||||||
t('Remove cross-filter')
|
</span>
|
||||||
) : (
|
),
|
||||||
<div>
|
disabled: isCrossFilterDisabled,
|
||||||
{t('Add cross-filter')}
|
onClick: () => {
|
||||||
<MenuItemTooltip
|
if (filters?.crossFilter) {
|
||||||
title={crossFilteringTooltipTitle}
|
dispatch(updateDataMask(id, filters.crossFilter.dataMask));
|
||||||
color={!isCrossFilterDisabled ? theme.colorIcon : undefined}
|
}
|
||||||
/>
|
},
|
||||||
</div>
|
},
|
||||||
)}
|
...(itemsCount > 1
|
||||||
</Menu.Item>
|
? [{ key: 'divider-1', type: 'divider' as const }]
|
||||||
{itemsCount > 1 && <Menu.Divider />}
|
: []),
|
||||||
</>,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (showDrillToDetail) {
|
if (showDrillToDetail) {
|
||||||
menuItems.push(
|
menuItems.push(...drillDetailMenuItems);
|
||||||
<DrillDetailMenuItems
|
|
||||||
formData={drillFormData}
|
|
||||||
filters={filters?.drillToDetail}
|
|
||||||
setFilters={setFilters}
|
|
||||||
isContextMenu
|
|
||||||
contextMenuY={clientY}
|
|
||||||
onSelection={onSelection}
|
|
||||||
submenuIndex={showCrossFilters ? 2 : 1}
|
|
||||||
setShowModal={setDrillModalIsOpen}
|
|
||||||
dataset={filteredDataset}
|
|
||||||
isLoadingDataset={isLoadingDataset}
|
|
||||||
{...(additionalConfig?.drillToDetail || {})}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDrillBy) {
|
if (showDrillBy) {
|
||||||
let submenuIndex = 0;
|
if (menuItems.length > 0) {
|
||||||
if (showCrossFilters) {
|
menuItems.push({ key: 'divider-drill-by', type: 'divider' as const });
|
||||||
submenuIndex += 1;
|
|
||||||
}
|
}
|
||||||
if (showDrillToDetail) {
|
|
||||||
submenuIndex += 2;
|
const hasDrillBy = enhancedFilters?.drillBy?.groupbyFieldName;
|
||||||
}
|
const handlesDimensionContextMenu = getChartMetadataRegistry()
|
||||||
menuItems.push(
|
.get(formData.viz_type)
|
||||||
<DrillByMenuItems
|
?.behaviors.find(behavior => behavior === Behavior.DrillBy);
|
||||||
drillByConfig={enhancedFilters?.drillBy}
|
const isDrillByDisabled = !handlesDimensionContextMenu || !hasDrillBy;
|
||||||
onSelection={onSelection}
|
|
||||||
onCloseMenu={closeContextMenu}
|
// Add a custom render component for DrillBy submenu to support react-window
|
||||||
formData={formData}
|
menuItems.push({
|
||||||
contextMenuY={clientY}
|
key: 'drill-by-submenu',
|
||||||
submenuIndex={submenuIndex}
|
disabled: isDrillByDisabled,
|
||||||
open={openKeys.includes('drill-by-submenu')}
|
label: (
|
||||||
key="drill-by-submenu"
|
<DrillBySubmenu
|
||||||
onDrillBy={handleDrillBy}
|
drillByConfig={enhancedFilters?.drillBy}
|
||||||
dataset={filteredDataset}
|
onSelection={onSelection}
|
||||||
isLoadingDataset={isLoadingDataset}
|
onCloseMenu={closeContextMenu}
|
||||||
{...(additionalConfig?.drillBy || {})}
|
formData={formData}
|
||||||
/>,
|
onDrillBy={handleDrillBy}
|
||||||
);
|
dataset={filteredDataset}
|
||||||
|
isLoadingDataset={isLoadingDataset}
|
||||||
|
{...(additionalConfig?.drillBy || {})}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
@@ -404,30 +406,22 @@ const ChartContextMenu = (
|
|||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
popupRender={() => (
|
menu={{
|
||||||
<Menu
|
items:
|
||||||
className="chart-context-menu"
|
menuItems.length > 0
|
||||||
data-test="chart-context-menu"
|
? menuItems
|
||||||
onOpenChange={setOpenKeys}
|
: [{ key: 'no-actions', label: t('No actions'), disabled: true }],
|
||||||
onClick={() => {
|
onClick: () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setOpenKeys([]);
|
onClose();
|
||||||
onClose();
|
},
|
||||||
}}
|
}}
|
||||||
>
|
dropdownRender={menu => (
|
||||||
{menuItems.length ? (
|
<div data-test="chart-context-menu">{menu}</div>
|
||||||
menuItems
|
|
||||||
) : (
|
|
||||||
<Menu.Item disabled>{t('No actions')}</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
)}
|
)}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
onOpenChange={value => {
|
onOpenChange={value => {
|
||||||
setVisible(value);
|
setVisible(value);
|
||||||
if (!value) {
|
|
||||||
setOpenKeys([]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
open={visible}
|
open={visible}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const ERROR_MESSAGE_COMPONENT = (props: ErrorMessageComponentProps) => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ChartErrorMessage', () => {
|
describe('ChartErrorMessage', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
chartId: 1,
|
chartId: 1,
|
||||||
@@ -49,7 +50,7 @@ describe('ChartErrorMessage', () => {
|
|||||||
source: 'test_source' as ChartSource,
|
source: 'test_source' as ChartSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders the default error message when error is null', () => {
|
test('renders the default error message when error is null', () => {
|
||||||
mockUseChartOwnerNames.mockReturnValue({
|
mockUseChartOwnerNames.mockReturnValue({
|
||||||
result: null,
|
result: null,
|
||||||
status: ResourceStatus.Loading,
|
status: ResourceStatus.Loading,
|
||||||
@@ -61,7 +62,7 @@ describe('ChartErrorMessage', () => {
|
|||||||
expect(screen.getByText('Test subtitle')).toBeInTheDocument();
|
expect(screen.getByText('Test subtitle')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the error message that is passed in from the error', () => {
|
test('renders the error message that is passed in from the error', () => {
|
||||||
getErrorMessageComponentRegistry().registerValue(
|
getErrorMessageComponentRegistry().registerValue(
|
||||||
'VALID_KEY',
|
'VALID_KEY',
|
||||||
ERROR_MESSAGE_COMPONENT,
|
ERROR_MESSAGE_COMPONENT,
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
CSSProperties,
|
|
||||||
ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Menu } from '@superset-ui/core/components/Menu';
|
|
||||||
import {
|
|
||||||
BaseFormData,
|
|
||||||
Behavior,
|
|
||||||
Column,
|
|
||||||
ContextMenuFilters,
|
|
||||||
css,
|
|
||||||
ensureIsArray,
|
|
||||||
getChartMetadataRegistry,
|
|
||||||
t,
|
|
||||||
useTheme,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { Constants, Input, Loading } from '@superset-ui/core/components';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
|
||||||
import { Icons } from '@superset-ui/core/components/Icons';
|
|
||||||
import { InputRef } from 'antd';
|
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
|
||||||
import { getSubmenuYOffset } from '../utils';
|
|
||||||
import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
|
|
||||||
import { Dataset } from '../types';
|
|
||||||
|
|
||||||
const SUBMENU_HEIGHT = 200;
|
|
||||||
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
|
|
||||||
const SEARCH_INPUT_HEIGHT = 48;
|
|
||||||
|
|
||||||
export interface DrillByMenuItemsProps {
|
|
||||||
drillByConfig?: ContextMenuFilters['drillBy'];
|
|
||||||
formData: BaseFormData & { [key: string]: any };
|
|
||||||
contextMenuY?: number;
|
|
||||||
submenuIndex?: number;
|
|
||||||
onSelection?: (...args: any) => void;
|
|
||||||
onClick?: (event: MouseEvent) => void;
|
|
||||||
onCloseMenu?: () => void;
|
|
||||||
openNewModal?: boolean;
|
|
||||||
excludedColumns?: Column[];
|
|
||||||
open: boolean;
|
|
||||||
onDrillBy?: (column: Column, dataset: Dataset) => void;
|
|
||||||
dataset?: Dataset;
|
|
||||||
isLoadingDataset?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DrillByMenuItems = ({
|
|
||||||
drillByConfig,
|
|
||||||
formData,
|
|
||||||
contextMenuY = 0,
|
|
||||||
submenuIndex = 0,
|
|
||||||
onSelection = () => {},
|
|
||||||
onClick = () => {},
|
|
||||||
onCloseMenu = () => {},
|
|
||||||
excludedColumns,
|
|
||||||
openNewModal = true,
|
|
||||||
open,
|
|
||||||
onDrillBy,
|
|
||||||
dataset,
|
|
||||||
isLoadingDataset = false,
|
|
||||||
...rest
|
|
||||||
}: DrillByMenuItemsProps) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [searchInput, setSearchInput] = useState('');
|
|
||||||
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
|
||||||
const ref = useRef<InputRef>(null);
|
|
||||||
const columns = dataset ? ensureIsArray(dataset.drillable_columns) : [];
|
|
||||||
const showSearch = columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
|
|
||||||
|
|
||||||
const handleSelection = useCallback(
|
|
||||||
(event, column) => {
|
|
||||||
onClick(event);
|
|
||||||
onSelection(column, drillByConfig);
|
|
||||||
if (openNewModal && onDrillBy && dataset) {
|
|
||||||
onDrillBy(column, dataset);
|
|
||||||
}
|
|
||||||
onCloseMenu();
|
|
||||||
},
|
|
||||||
[drillByConfig, onClick, onSelection, openNewModal, onDrillBy, dataset],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
ref.current?.input?.focus({ preventScroll: true });
|
|
||||||
} else {
|
|
||||||
// Reset search input when menu is closed
|
|
||||||
setSearchInput('');
|
|
||||||
setDebouncedSearchInput('');
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const hasDrillBy = drillByConfig?.groupbyFieldName;
|
|
||||||
|
|
||||||
const handlesDimensionContextMenu = useMemo(
|
|
||||||
() =>
|
|
||||||
getChartMetadataRegistry()
|
|
||||||
.get(formData.viz_type)
|
|
||||||
?.behaviors.find(behavior => behavior === Behavior.DrillBy),
|
|
||||||
[formData.viz_type],
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedSetSearchInput = useMemo(
|
|
||||||
() =>
|
|
||||||
debounce((value: string) => {
|
|
||||||
setDebouncedSearchInput(value);
|
|
||||||
}, Constants.FAST_DEBOUNCE),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleInput = (value: string) => {
|
|
||||||
setSearchInput(value);
|
|
||||||
debouncedSetSearchInput(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredColumns = useMemo(
|
|
||||||
() =>
|
|
||||||
columns.filter(column =>
|
|
||||||
(column.verbose_name || column.column_name)
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(debouncedSearchInput.toLowerCase()),
|
|
||||||
),
|
|
||||||
[columns, debouncedSearchInput],
|
|
||||||
);
|
|
||||||
|
|
||||||
const submenuYOffset = useMemo(
|
|
||||||
() =>
|
|
||||||
getSubmenuYOffset(
|
|
||||||
contextMenuY,
|
|
||||||
filteredColumns.length || 1,
|
|
||||||
submenuIndex,
|
|
||||||
SUBMENU_HEIGHT,
|
|
||||||
showSearch ? SEARCH_INPUT_HEIGHT : 0,
|
|
||||||
),
|
|
||||||
[contextMenuY, filteredColumns.length, submenuIndex, showSearch],
|
|
||||||
);
|
|
||||||
|
|
||||||
let tooltip: ReactNode;
|
|
||||||
|
|
||||||
if (!handlesDimensionContextMenu) {
|
|
||||||
tooltip = t('Drill by is not yet supported for this chart type');
|
|
||||||
} else if (!hasDrillBy) {
|
|
||||||
tooltip = t('Drill by is not available for this data point');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handlesDimensionContextMenu || !hasDrillBy) {
|
|
||||||
return (
|
|
||||||
<Menu.Item key="drill-by-disabled" disabled {...rest}>
|
|
||||||
<div>
|
|
||||||
{t('Drill by')}
|
|
||||||
<MenuItemTooltip title={tooltip} />
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Row = ({
|
|
||||||
index,
|
|
||||||
data,
|
|
||||||
style,
|
|
||||||
}: {
|
|
||||||
index: number;
|
|
||||||
data: { columns: Column[] };
|
|
||||||
style: CSSProperties;
|
|
||||||
}) => {
|
|
||||||
const { columns, ...rest } = data;
|
|
||||||
const column = columns[index];
|
|
||||||
return (
|
|
||||||
<VirtualizedMenuItem
|
|
||||||
tooltipText={column.verbose_name || column.column_name}
|
|
||||||
onClick={e => handleSelection(e, column)}
|
|
||||||
style={style}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{column.verbose_name || column.column_name}
|
|
||||||
</VirtualizedMenuItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Don't render drill by menu items when matrixify is enabled
|
|
||||||
if (
|
|
||||||
formData.matrixify_enable_vertical_layout === true ||
|
|
||||||
formData.matrixify_enable_horizontal_layout === true
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Menu.SubMenu
|
|
||||||
key="drill-by-submenu"
|
|
||||||
title={t('Drill by')}
|
|
||||||
popupClassName="chart-context-submenu"
|
|
||||||
popupOffset={[0, submenuYOffset]}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<div data-test="drill-by-submenu">
|
|
||||||
{showSearch && (
|
|
||||||
<Input
|
|
||||||
ref={ref}
|
|
||||||
prefix={
|
|
||||||
<Icons.SearchOutlined
|
|
||||||
iconSize="l"
|
|
||||||
iconColor={theme.colorIcon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onChange={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleInput(e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder={t('Search columns')}
|
|
||||||
onClick={e => {
|
|
||||||
// prevent closing menu when clicking on input
|
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
|
||||||
}}
|
|
||||||
allowClear
|
|
||||||
css={css`
|
|
||||||
width: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 3}px;
|
|
||||||
box-shadow: none;
|
|
||||||
`}
|
|
||||||
value={searchInput}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLoadingDataset ? (
|
|
||||||
<div
|
|
||||||
css={css`
|
|
||||||
padding: ${theme.sizeUnit * 3}px 0;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Loading position="inline-centered" />
|
|
||||||
</div>
|
|
||||||
) : filteredColumns.length ? (
|
|
||||||
<List
|
|
||||||
width="100%"
|
|
||||||
height={SUBMENU_HEIGHT}
|
|
||||||
itemSize={35}
|
|
||||||
itemCount={filteredColumns.length}
|
|
||||||
itemData={{ columns: filteredColumns, ...rest }}
|
|
||||||
overscanCount={20}
|
|
||||||
>
|
|
||||||
{Row}
|
|
||||||
</List>
|
|
||||||
) : (
|
|
||||||
<Menu.Item disabled key="no-drill-by-columns-found" {...rest}>
|
|
||||||
{t('No columns found')}
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Menu.SubMenu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -281,6 +281,7 @@ test('should render "Edit chart" enabled with can_explore permission', async ()
|
|||||||
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Embedded mode behavior', () => {
|
describe('Embedded mode behavior', () => {
|
||||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||||
const { isEmbedded } = require('src/dashboard/util/isEmbedded');
|
const { isEmbedded } = require('src/dashboard/util/isEmbedded');
|
||||||
@@ -357,6 +358,7 @@ describe('Embedded mode behavior', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Table view with pagination', () => {
|
describe('Table view with pagination', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Mock a large dataset response for pagination testing
|
// Mock a large dataset response for pagination testing
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||||
import { Menu } from '@superset-ui/core/components/Menu';
|
|
||||||
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
|
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
|
||||||
import { DrillByMenuItems, DrillByMenuItemsProps } from './DrillByMenuItems';
|
import { DrillBySubmenu, DrillBySubmenuProps } from './DrillBySubmenu';
|
||||||
|
|
||||||
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
|
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
|
||||||
|
|
||||||
@@ -79,37 +78,29 @@ const defaultFilters = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderMenu = ({
|
const renderSubmenu = ({
|
||||||
formData = defaultFormData,
|
formData = defaultFormData,
|
||||||
drillByConfig = { filters: defaultFilters, groupbyFieldName: 'groupby' },
|
drillByConfig = { filters: defaultFilters, groupbyFieldName: 'groupby' },
|
||||||
dataset = mockDataset,
|
dataset = mockDataset,
|
||||||
...rest
|
...rest
|
||||||
}: Partial<DrillByMenuItemsProps>) =>
|
}: Partial<DrillBySubmenuProps>) =>
|
||||||
render(
|
render(
|
||||||
<Menu forceSubMenuRender>
|
<DrillBySubmenu
|
||||||
<DrillByMenuItems
|
formData={formData ?? defaultFormData}
|
||||||
formData={formData ?? defaultFormData}
|
drillByConfig={drillByConfig}
|
||||||
drillByConfig={drillByConfig}
|
dataset={dataset}
|
||||||
dataset={dataset}
|
{...rest}
|
||||||
open
|
/>,
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</Menu>,
|
|
||||||
{ useRouter: true, useRedux: true },
|
{ useRouter: true, useRedux: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectDrillByDisabled = async (tooltipContent: string) => {
|
const expectDrillByDisabled = async (tooltipContent: string) => {
|
||||||
const drillByMenuItem = screen
|
const drillByButton = screen.getByRole('button', { name: /drill by/i });
|
||||||
.getAllByRole('menuitem')
|
expect(drillByButton).toBeInTheDocument();
|
||||||
.find(menuItem => within(menuItem).queryByText('Drill by'));
|
expect(drillByButton).toBeVisible();
|
||||||
|
expect(drillByButton).toHaveAttribute('tabindex', '-1');
|
||||||
|
|
||||||
expect(drillByMenuItem).toBeDefined();
|
const tooltipTrigger = within(drillByButton).getByTestId('tooltip-trigger');
|
||||||
expect(drillByMenuItem).toBeVisible();
|
|
||||||
expect(drillByMenuItem).toHaveAttribute('aria-disabled', 'true');
|
|
||||||
|
|
||||||
const tooltipTrigger = within(drillByMenuItem!).getByTestId(
|
|
||||||
'tooltip-trigger',
|
|
||||||
);
|
|
||||||
userEvent.hover(tooltipTrigger as HTMLElement);
|
userEvent.hover(tooltipTrigger as HTMLElement);
|
||||||
|
|
||||||
const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
|
const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
|
||||||
@@ -117,20 +108,17 @@ const expectDrillByDisabled = async (tooltipContent: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const expectDrillByEnabled = async () => {
|
const expectDrillByEnabled = async () => {
|
||||||
const drillByMenuItem = screen.getByRole('menuitem', {
|
const drillByButton = screen.getByRole('button', { name: /drill by/i });
|
||||||
name: 'Drill by',
|
expect(drillByButton).toBeInTheDocument();
|
||||||
});
|
expect(drillByButton).not.toHaveAttribute('tabindex', '-1');
|
||||||
expect(drillByMenuItem).toBeInTheDocument();
|
|
||||||
await waitFor(() =>
|
const tooltipTrigger = within(drillByButton).queryByTestId('tooltip-trigger');
|
||||||
expect(drillByMenuItem).not.toHaveAttribute('aria-disabled'),
|
|
||||||
);
|
|
||||||
const tooltipTrigger =
|
|
||||||
within(drillByMenuItem).queryByTestId('tooltip-trigger');
|
|
||||||
expect(tooltipTrigger).not.toBeInTheDocument();
|
expect(tooltipTrigger).not.toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.hover(within(drillByMenuItem).getByText('Drill by'));
|
userEvent.hover(drillByButton);
|
||||||
const drillBySubmenus = await screen.findAllByTestId('drill-by-submenu');
|
|
||||||
expect(drillBySubmenus[0]).toBeInTheDocument();
|
const popover = await screen.findByRole('menu');
|
||||||
|
expect(popover).toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
getChartMetadataRegistry().registerValue(
|
getChartMetadataRegistry().registerValue(
|
||||||
@@ -149,7 +137,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('render disabled menu item for unsupported chart', async () => {
|
test('render disabled menu item for unsupported chart', async () => {
|
||||||
renderMenu({
|
renderSubmenu({
|
||||||
formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
|
formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
|
||||||
});
|
});
|
||||||
await expectDrillByDisabled(
|
await expectDrillByDisabled(
|
||||||
@@ -158,89 +146,75 @@ test('render disabled menu item for unsupported chart', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('render enabled menu item for supported chart, no filters', async () => {
|
test('render enabled menu item for supported chart, no filters', async () => {
|
||||||
renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } });
|
renderSubmenu({
|
||||||
|
drillByConfig: { filters: [], groupbyFieldName: 'groupby' },
|
||||||
|
});
|
||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render disabled menu item for supported chart, no columns', async () => {
|
test('render disabled menu item for supported chart, no columns', async () => {
|
||||||
const emptyDataset = { ...mockDataset, columns: [], drillable_columns: [] };
|
const emptyDataset = { ...mockDataset, columns: [], drillable_columns: [] };
|
||||||
renderMenu({ dataset: emptyDataset });
|
renderSubmenu({ dataset: emptyDataset });
|
||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
screen.getByText('No columns found');
|
|
||||||
|
const noColumnsText = await screen.findByText('No columns found');
|
||||||
|
expect(noColumnsText).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render menu item with submenu without searchbox', async () => {
|
test('render menu item with submenu without searchbox', async () => {
|
||||||
const slicedColumns = defaultColumns.slice(0, 9);
|
const slicedColumns = defaultColumns.slice(0, 1); // Use only 1 column to avoid search box
|
||||||
const datasetWithSlicedColumns = {
|
const datasetWithSlicedColumns = {
|
||||||
...mockDataset,
|
...mockDataset,
|
||||||
columns: slicedColumns,
|
columns: slicedColumns,
|
||||||
drillable_columns: slicedColumns,
|
drillable_columns: slicedColumns,
|
||||||
};
|
};
|
||||||
renderMenu({ dataset: datasetWithSlicedColumns });
|
renderSubmenu({ dataset: datasetWithSlicedColumns });
|
||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
|
|
||||||
// Check that each column appears in the drill-by submenu
|
// Check that the column appears in the popover
|
||||||
slicedColumns.forEach(column => {
|
const col1Element = await screen.findByText('col1');
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
expect(col1Element).toBeInTheDocument();
|
||||||
const submenu = submenus[0]; // Use the first submenu
|
|
||||||
expect(within(submenu).getByText(column.column_name)).toBeInTheDocument();
|
// Should not have search box for small number of columns
|
||||||
});
|
|
||||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add global timeout for all tests
|
|
||||||
jest.setTimeout(20000);
|
|
||||||
|
|
||||||
test('render menu item with submenu and searchbox', async () => {
|
test('render menu item with submenu and searchbox', async () => {
|
||||||
renderMenu({ dataset: mockDataset });
|
renderSubmenu({ dataset: mockDataset });
|
||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
|
|
||||||
// Wait for all columns to be visible
|
// Wait for first column to ensure menu is loaded
|
||||||
await waitFor(
|
await screen.findByText('col1');
|
||||||
() => {
|
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
|
||||||
const submenu = submenus[0];
|
|
||||||
defaultColumns.forEach(column => {
|
|
||||||
expect(
|
|
||||||
within(submenu).getByText(column.column_name),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ timeout: 10000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchbox = await waitFor(
|
// Then check all columns are visible
|
||||||
() => screen.getAllByPlaceholderText('Search columns')[0],
|
defaultColumns.forEach(column => {
|
||||||
);
|
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchbox = screen.getByPlaceholderText('Search columns');
|
||||||
expect(searchbox).toBeInTheDocument();
|
expect(searchbox).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.type(searchbox, 'col1');
|
userEvent.type(searchbox, 'col1');
|
||||||
|
|
||||||
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
|
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
|
||||||
|
|
||||||
// Wait for filtered results
|
// Wait for filtering to take effect by checking for first filtered item
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
// Check that non-matching columns are not visible
|
||||||
const submenu = submenus[0];
|
expect(screen.queryByText('col2')).not.toBeInTheDocument();
|
||||||
expectedFilteredColumnNames.forEach(colName => {
|
|
||||||
expect(within(submenu).getByText(colName)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
// Then verify all expected columns are visible
|
||||||
const submenu = submenus[0];
|
expectedFilteredColumnNames.forEach(colName => {
|
||||||
|
expect(screen.getByText(colName)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that non-matching columns are not visible
|
||||||
defaultColumns
|
defaultColumns
|
||||||
.filter(col => !expectedFilteredColumnNames.includes(col.column_name))
|
.filter(col => !expectedFilteredColumnNames.includes(col.column_name))
|
||||||
.forEach(col => {
|
.forEach(col => {
|
||||||
expect(
|
expect(screen.queryByText(col.column_name)).not.toBeInTheDocument();
|
||||||
within(submenu).queryByText(col.column_name),
|
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expectedFilteredColumnNames.forEach(colName => {
|
|
||||||
expect(within(submenu).getByText(colName)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Do not display excluded column in the menu', async () => {
|
test('Do not display excluded column in the menu', async () => {
|
||||||
@@ -252,7 +226,7 @@ test('Do not display excluded column in the menu', async () => {
|
|||||||
...mockDataset,
|
...mockDataset,
|
||||||
drillable_columns: filteredColumns,
|
drillable_columns: filteredColumns,
|
||||||
};
|
};
|
||||||
renderMenu({
|
renderSubmenu({
|
||||||
dataset: datasetWithFilteredColumns,
|
dataset: datasetWithFilteredColumns,
|
||||||
excludedColumns: excludedColNames.map(colName => ({
|
excludedColumns: excludedColNames.map(colName => ({
|
||||||
column_name: colName,
|
column_name: colName,
|
||||||
@@ -261,32 +235,24 @@ test('Do not display excluded column in the menu', async () => {
|
|||||||
|
|
||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
|
|
||||||
// Wait for menu items to be loaded
|
// Wait for first column to ensure menu is loaded
|
||||||
await waitFor(
|
await screen.findByText('col1');
|
||||||
() => {
|
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
// Then check all non-excluded columns are visible
|
||||||
const submenu = submenus[0];
|
defaultColumns
|
||||||
defaultColumns
|
.filter(column => !excludedColNames.includes(column.column_name))
|
||||||
.filter(column => !excludedColNames.includes(column.column_name))
|
.forEach(column => {
|
||||||
.forEach(column => {
|
expect(screen.getByText(column.column_name)).toBeInTheDocument();
|
||||||
expect(
|
});
|
||||||
within(submenu).getByText(column.column_name),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ timeout: 10000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
|
||||||
const submenu = submenus[0];
|
|
||||||
excludedColNames.forEach(colName => {
|
excludedColNames.forEach(colName => {
|
||||||
expect(within(submenu).queryByText(colName)).not.toBeInTheDocument();
|
expect(screen.queryByText(colName)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('When menu item is clicked, call onSelection with clicked column and drill by filters', async () => {
|
test('When menu item is clicked, call onSelection with clicked column and drill by filters', async () => {
|
||||||
const onSelectionMock = jest.fn();
|
const onSelectionMock = jest.fn();
|
||||||
renderMenu({
|
renderSubmenu({
|
||||||
dataset: mockDataset,
|
dataset: mockDataset,
|
||||||
onSelection: onSelectionMock,
|
onSelection: onSelectionMock,
|
||||||
});
|
});
|
||||||
@@ -294,11 +260,7 @@ test('When menu item is clicked, call onSelection with clicked column and drill
|
|||||||
await expectDrillByEnabled();
|
await expectDrillByEnabled();
|
||||||
|
|
||||||
// Wait for col1 to be visible before clicking
|
// Wait for col1 to be visible before clicking
|
||||||
const col1Element = await waitFor(() => {
|
const col1Element = await screen.findByText('col1');
|
||||||
const submenus = screen.getAllByTestId('drill-by-submenu');
|
|
||||||
const submenu = submenus[0];
|
|
||||||
return within(submenu).getByText('col1');
|
|
||||||
});
|
|
||||||
userEvent.click(col1Element);
|
userEvent.click(col1Element);
|
||||||
|
|
||||||
expect(onSelectionMock).toHaveBeenCalledWith(
|
expect(onSelectionMock).toHaveBeenCalledWith(
|
||||||
@@ -309,3 +271,10 @@ test('When menu item is clicked, call onSelection with clicked column and drill
|
|||||||
{ filters: defaultFilters, groupbyFieldName: 'groupby' },
|
{ filters: defaultFilters, groupbyFieldName: 'groupby' },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('matrixify_enable_vertical_layout should not render component', () => {
|
||||||
|
const { container } = renderSubmenu({
|
||||||
|
formData: { ...defaultFormData, matrixify_enable_vertical_layout: true },
|
||||||
|
});
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
BaseFormData,
|
||||||
|
Behavior,
|
||||||
|
Column,
|
||||||
|
ContextMenuFilters,
|
||||||
|
css,
|
||||||
|
ensureIsArray,
|
||||||
|
getChartMetadataRegistry,
|
||||||
|
t,
|
||||||
|
useTheme,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import {
|
||||||
|
Constants,
|
||||||
|
Input,
|
||||||
|
Loading,
|
||||||
|
Popover,
|
||||||
|
Icons,
|
||||||
|
} from '@superset-ui/core/components';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import { InputRef } from 'antd';
|
||||||
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
|
import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
|
||||||
|
import { Dataset } from '../types';
|
||||||
|
|
||||||
|
const SUBMENU_HEIGHT = 200;
|
||||||
|
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
|
||||||
|
|
||||||
|
export interface DrillBySubmenuProps {
|
||||||
|
drillByConfig?: ContextMenuFilters['drillBy'];
|
||||||
|
formData: BaseFormData & { [key: string]: any };
|
||||||
|
onSelection?: (...args: any) => void;
|
||||||
|
onClick?: (event: MouseEvent) => void;
|
||||||
|
onCloseMenu?: () => void;
|
||||||
|
openNewModal?: boolean;
|
||||||
|
excludedColumns?: Column[];
|
||||||
|
onDrillBy?: (column: Column, dataset: Dataset) => void;
|
||||||
|
dataset?: Dataset;
|
||||||
|
isLoadingDataset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DrillBySubmenu = ({
|
||||||
|
drillByConfig,
|
||||||
|
formData,
|
||||||
|
onSelection = () => {},
|
||||||
|
onClick = () => {},
|
||||||
|
onCloseMenu = () => {},
|
||||||
|
openNewModal = true,
|
||||||
|
excludedColumns,
|
||||||
|
onDrillBy,
|
||||||
|
dataset,
|
||||||
|
isLoadingDataset = false,
|
||||||
|
...rest
|
||||||
|
}: DrillBySubmenuProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [searchInput, setSearchInput] = useState('');
|
||||||
|
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const ref = useRef<InputRef>(null);
|
||||||
|
const menuItemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => (dataset ? ensureIsArray(dataset.drillable_columns) : []),
|
||||||
|
[dataset],
|
||||||
|
);
|
||||||
|
const showSearch = columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
|
||||||
|
|
||||||
|
const handleSelection = useCallback(
|
||||||
|
(event, column) => {
|
||||||
|
onClick(event as MouseEvent);
|
||||||
|
onSelection(column, drillByConfig);
|
||||||
|
if (openNewModal && onDrillBy && dataset) {
|
||||||
|
onDrillBy(column, dataset);
|
||||||
|
}
|
||||||
|
setPopoverOpen(false);
|
||||||
|
onCloseMenu();
|
||||||
|
},
|
||||||
|
[
|
||||||
|
drillByConfig,
|
||||||
|
onClick,
|
||||||
|
onSelection,
|
||||||
|
openNewModal,
|
||||||
|
onDrillBy,
|
||||||
|
dataset,
|
||||||
|
onCloseMenu,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
if (popoverOpen) {
|
||||||
|
// Small delay to ensure popover is rendered
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
ref.current?.input?.focus({ preventScroll: true });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
// Reset search input when menu is closed
|
||||||
|
setSearchInput('');
|
||||||
|
setDebouncedSearchInput('');
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, [popoverOpen]);
|
||||||
|
|
||||||
|
const hasDrillBy = drillByConfig?.groupbyFieldName;
|
||||||
|
|
||||||
|
const handlesDimensionContextMenu = useMemo(
|
||||||
|
() =>
|
||||||
|
getChartMetadataRegistry()
|
||||||
|
.get(formData.viz_type)
|
||||||
|
?.behaviors.find(behavior => behavior === Behavior.DrillBy),
|
||||||
|
[formData.viz_type],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedSetSearchInput = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((value: string) => {
|
||||||
|
setDebouncedSearchInput(value);
|
||||||
|
}, Constants.FAST_DEBOUNCE),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInput = (value: string) => {
|
||||||
|
setSearchInput(value);
|
||||||
|
debouncedSetSearchInput(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredColumns = useMemo(
|
||||||
|
() =>
|
||||||
|
columns
|
||||||
|
.filter(column => {
|
||||||
|
// Filter out excluded columns
|
||||||
|
const excludedColumnNames =
|
||||||
|
excludedColumns?.map(col => col.column_name) || [];
|
||||||
|
return !excludedColumnNames.includes(column.column_name);
|
||||||
|
})
|
||||||
|
.filter(column =>
|
||||||
|
(column.verbose_name || column.column_name)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(debouncedSearchInput.toLowerCase()),
|
||||||
|
),
|
||||||
|
[columns, debouncedSearchInput, excludedColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
let tooltip: ReactNode;
|
||||||
|
|
||||||
|
if (!handlesDimensionContextMenu) {
|
||||||
|
tooltip = t('Drill by is not yet supported for this chart type');
|
||||||
|
} else if (!hasDrillBy) {
|
||||||
|
tooltip = t('Drill by is not available for this data point');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
formData.matrixify_enable_vertical_layout === true ||
|
||||||
|
formData.matrixify_enable_horizontal_layout === true
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDisabled = !handlesDimensionContextMenu || !hasDrillBy;
|
||||||
|
|
||||||
|
const Row = ({
|
||||||
|
index,
|
||||||
|
data,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
data: { columns: Column[] };
|
||||||
|
style: CSSProperties;
|
||||||
|
}) => {
|
||||||
|
const { columns } = data;
|
||||||
|
const column = columns[index];
|
||||||
|
return (
|
||||||
|
<VirtualizedMenuItem
|
||||||
|
tooltipText={column.verbose_name || column.column_name}
|
||||||
|
onClick={e => handleSelection(e, column)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{column.verbose_name || column.column_name}
|
||||||
|
</VirtualizedMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popoverContent = (
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
tabIndex={0}
|
||||||
|
data-test="drill-by-submenu"
|
||||||
|
css={css`
|
||||||
|
width: 220px;
|
||||||
|
max-width: 220px;
|
||||||
|
.ant-input-affix-wrapper {
|
||||||
|
margin-bottom: ${theme.sizeUnit * 2}px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{showSearch && (
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
prefix={
|
||||||
|
<Icons.SearchOutlined iconSize="l" iconColor={theme.colorIcon} />
|
||||||
|
}
|
||||||
|
onChange={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleInput(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder={t('Search columns')}
|
||||||
|
onClick={e => {
|
||||||
|
// prevent closing menu when clicking on input
|
||||||
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
css={css`
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: none;
|
||||||
|
`}
|
||||||
|
value={searchInput}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isLoadingDataset ? (
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
padding: ${theme.sizeUnit * 3}px 0;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Loading position="inline-centered" />
|
||||||
|
</div>
|
||||||
|
) : filteredColumns.length ? (
|
||||||
|
<List
|
||||||
|
width="100%"
|
||||||
|
height={SUBMENU_HEIGHT}
|
||||||
|
itemSize={35}
|
||||||
|
itemCount={filteredColumns.length}
|
||||||
|
itemData={{ columns: filteredColumns }}
|
||||||
|
overscanCount={20}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
css={css`
|
||||||
|
padding: ${theme.sizeUnit * 2}px;
|
||||||
|
color: ${theme.colorTextDisabled};
|
||||||
|
text-align: center;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{t('No columns found')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const menuItem = (
|
||||||
|
<div
|
||||||
|
ref={menuItemRef}
|
||||||
|
role="button"
|
||||||
|
tabIndex={isDisabled ? -1 : 0}
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
||||||
|
color: ${isDisabled ? theme.colorTextDisabled : 'inherit'};
|
||||||
|
&:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onClick={() => !isDisabled && setPopoverOpen(!popoverOpen)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (!isDisabled && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault();
|
||||||
|
setPopoverOpen(!popoverOpen);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{t('Drill by')}</span>
|
||||||
|
{isDisabled ? (
|
||||||
|
<MenuItemTooltip title={tooltip} />
|
||||||
|
) : (
|
||||||
|
<Icons.RightOutlined iconSize="s" iconColor={theme.colorTextTertiary} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={popoverContent}
|
||||||
|
placement="rightTop"
|
||||||
|
open={popoverOpen}
|
||||||
|
onOpenChange={setPopoverOpen}
|
||||||
|
trigger={['hover', 'click']}
|
||||||
|
arrow={false}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: theme.sizeUnit * 2,
|
||||||
|
boxShadow: theme.boxShadow,
|
||||||
|
borderRadius: theme.borderRadius,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{menuItem}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
ReactNode,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
} from 'react';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import {
|
|
||||||
Behavior,
|
|
||||||
BinaryQueryObjectFilterClause,
|
|
||||||
css,
|
|
||||||
extractQueryFields,
|
|
||||||
getChartMetadataRegistry,
|
|
||||||
QueryFormData,
|
|
||||||
removeHTMLTags,
|
|
||||||
styled,
|
|
||||||
t,
|
|
||||||
} from '@superset-ui/core';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { Menu } from '@superset-ui/core/components/Menu';
|
|
||||||
import { RootState } from 'src/dashboard/types';
|
|
||||||
import { getSubmenuYOffset } from '../utils';
|
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
|
||||||
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
|
|
||||||
import { Dataset } from '../types';
|
|
||||||
|
|
||||||
const DRILL_TO_DETAIL = t('Drill to detail');
|
|
||||||
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
|
|
||||||
const DISABLED_REASONS = {
|
|
||||||
DATABASE: t(
|
|
||||||
'Drill to detail is disabled for this database. Change the database settings to enable it.',
|
|
||||||
),
|
|
||||||
NO_AGGREGATIONS: t(
|
|
||||||
'Drill to detail is disabled because this chart does not group data by dimension value.',
|
|
||||||
),
|
|
||||||
NO_FILTERS: t(
|
|
||||||
'Right-click on a dimension value to drill to detail by that value.',
|
|
||||||
),
|
|
||||||
NOT_SUPPORTED: t(
|
|
||||||
'Drill to detail by value is not yet supported for this chart type.',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const DisabledMenuItem = ({
|
|
||||||
children,
|
|
||||||
menuKey,
|
|
||||||
...rest
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
menuKey: string;
|
|
||||||
}) => (
|
|
||||||
<Menu.Item disabled key={menuKey} {...rest}>
|
|
||||||
<div
|
|
||||||
css={css`
|
|
||||||
white-space: normal;
|
|
||||||
max-width: 160px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Filter = ({
|
|
||||||
children,
|
|
||||||
stripHTML = false,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
stripHTML: boolean;
|
|
||||||
}) => {
|
|
||||||
const content =
|
|
||||||
stripHTML && typeof children === 'string'
|
|
||||||
? removeHTMLTags(children)
|
|
||||||
: children;
|
|
||||||
return <span>{content}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledFilter = styled(Filter)`
|
|
||||||
${({ theme }) => `
|
|
||||||
font-weight: ${theme.fontWeightStrong};
|
|
||||||
color: ${theme.colorPrimary};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type DrillDetailMenuItemsProps = {
|
|
||||||
formData: QueryFormData;
|
|
||||||
filters?: BinaryQueryObjectFilterClause[];
|
|
||||||
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
|
|
||||||
isContextMenu?: boolean;
|
|
||||||
contextMenuY?: number;
|
|
||||||
onSelection?: () => void;
|
|
||||||
onClick?: (event: MouseEvent) => void;
|
|
||||||
submenuIndex?: number;
|
|
||||||
setShowModal: (show: boolean) => void;
|
|
||||||
key?: string;
|
|
||||||
forceSubmenuRender?: boolean;
|
|
||||||
dataset?: Dataset;
|
|
||||||
isLoadingDataset?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DrillDetailMenuItems = ({
|
|
||||||
formData,
|
|
||||||
filters = [],
|
|
||||||
isContextMenu = false,
|
|
||||||
contextMenuY = 0,
|
|
||||||
onSelection = () => null,
|
|
||||||
onClick = () => null,
|
|
||||||
submenuIndex = 0,
|
|
||||||
setFilters,
|
|
||||||
setShowModal,
|
|
||||||
key,
|
|
||||||
...props
|
|
||||||
}: DrillDetailMenuItemsProps) => {
|
|
||||||
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
|
|
||||||
({ datasources }) =>
|
|
||||||
datasources[formData.datasource]?.database?.disable_drill_to_detail,
|
|
||||||
);
|
|
||||||
|
|
||||||
const openModal = useCallback(
|
|
||||||
(filters, event) => {
|
|
||||||
onClick(event);
|
|
||||||
onSelection();
|
|
||||||
setFilters(filters);
|
|
||||||
setShowModal(true);
|
|
||||||
},
|
|
||||||
[onClick, onSelection],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu`
|
|
||||||
// event for dimensions. If it doesn't, tell the user that drill to detail by
|
|
||||||
// dimension is not supported. If it does, and the `contextmenu` handler didn't
|
|
||||||
// pass any filters, tell the user that they didn't select a dimension.
|
|
||||||
const handlesDimensionContextMenu = useMemo(
|
|
||||||
() =>
|
|
||||||
getChartMetadataRegistry()
|
|
||||||
.get(formData.viz_type)
|
|
||||||
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail),
|
|
||||||
[formData.viz_type],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check metrics to see if chart's current configuration lacks
|
|
||||||
// aggregations, in which case Drill to Detail should be disabled.
|
|
||||||
const noAggregations = useMemo(() => {
|
|
||||||
const { metrics } = extractQueryFields(formData);
|
|
||||||
return isEmpty(metrics);
|
|
||||||
}, [formData]);
|
|
||||||
|
|
||||||
// Ensure submenu doesn't appear offscreen
|
|
||||||
const submenuYOffset = useMemo(
|
|
||||||
() =>
|
|
||||||
getSubmenuYOffset(
|
|
||||||
contextMenuY,
|
|
||||||
filters.length > 1 ? filters.length + 1 : filters.length,
|
|
||||||
submenuIndex,
|
|
||||||
),
|
|
||||||
[contextMenuY, filters.length, submenuIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
let drillDisabled;
|
|
||||||
let drillByDisabled;
|
|
||||||
if (drillToDetailDisabled) {
|
|
||||||
drillDisabled = DISABLED_REASONS.DATABASE;
|
|
||||||
drillByDisabled = DISABLED_REASONS.DATABASE;
|
|
||||||
} else if (handlesDimensionContextMenu) {
|
|
||||||
if (noAggregations) {
|
|
||||||
drillDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
|
|
||||||
drillByDisabled = DISABLED_REASONS.NO_AGGREGATIONS;
|
|
||||||
} else if (!filters?.length) {
|
|
||||||
drillByDisabled = DISABLED_REASONS.NO_FILTERS;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
const drillToDetailMenuItem = drillDisabled ? (
|
|
||||||
<DisabledMenuItem menuKey="drill-to-detail-disabled" {...props}>
|
|
||||||
{DRILL_TO_DETAIL}
|
|
||||||
<MenuItemTooltip title={drillDisabled} />
|
|
||||||
</DisabledMenuItem>
|
|
||||||
) : (
|
|
||||||
<Menu.Item key="drill-to-detail" onClick={openModal.bind(null, [])}>
|
|
||||||
{DRILL_TO_DETAIL}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
|
|
||||||
const drillToDetailByMenuItem = drillByDisabled ? (
|
|
||||||
<DisabledMenuItem menuKey="drill-to-detail-by-disabled" {...props}>
|
|
||||||
{DRILL_TO_DETAIL_BY}
|
|
||||||
<MenuItemTooltip title={drillByDisabled} />
|
|
||||||
</DisabledMenuItem>
|
|
||||||
) : (
|
|
||||||
<Menu.SubMenu
|
|
||||||
popupOffset={[0, submenuYOffset]}
|
|
||||||
popupClassName="chart-context-submenu"
|
|
||||||
title={DRILL_TO_DETAIL_BY}
|
|
||||||
key={key}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div data-test="drill-to-detail-by-submenu">
|
|
||||||
{filters.map((filter, i) => (
|
|
||||||
<MenuItemWithTruncation
|
|
||||||
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
|
||||||
menuKey={`drill-detail-filter-${i}`}
|
|
||||||
onClick={openModal.bind(null, [filter])}
|
|
||||||
>
|
|
||||||
{`${DRILL_TO_DETAIL_BY} `}
|
|
||||||
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
|
|
||||||
</MenuItemWithTruncation>
|
|
||||||
))}
|
|
||||||
{filters.length > 1 && (
|
|
||||||
<Menu.Item
|
|
||||||
key="drill-detail-filter-all"
|
|
||||||
onClick={openModal.bind(null, filters)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{`${DRILL_TO_DETAIL_BY} `}
|
|
||||||
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Menu.SubMenu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{drillToDetailMenuItem}
|
|
||||||
{isContextMenu && drillToDetailByMenuItem}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DrillDetailMenuItems;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { default as DrillDetailMenuItems } from './DrillDetailMenuItems';
|
|
||||||
export { useDrillDetailMenuItems } from './useDrillDetailMenuItems';
|
|
||||||
@@ -59,6 +59,7 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
getChartBuildQueryRegistry: jest.fn(),
|
getChartBuildQueryRegistry: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('chart actions', () => {
|
describe('chart actions', () => {
|
||||||
const MOCK_URL = '/mockURL';
|
const MOCK_URL = '/mockURL';
|
||||||
let dispatch;
|
let dispatch;
|
||||||
@@ -121,12 +122,13 @@ describe('chart actions', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('v1 API', () => {
|
describe('v1 API', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false };
|
fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should query with the built query', async () => {
|
test('should query with the built query', async () => {
|
||||||
const actionThunk = actions.postChartFormData({}, null);
|
const actionThunk = actions.postChartFormData({}, null);
|
||||||
await actionThunk(dispatch, mockGetState);
|
await actionThunk(dispatch, mockGetState);
|
||||||
|
|
||||||
@@ -141,7 +143,7 @@ describe('chart actions', () => {
|
|||||||
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle the bigint without regression', async () => {
|
test('should handle the bigint without regression', async () => {
|
||||||
getChartDataUriStub.restore();
|
getChartDataUriStub.restore();
|
||||||
const mockBigIntUrl = '/mock/chart/data/bigint';
|
const mockBigIntUrl = '/mock/chart/data/bigint';
|
||||||
const expectedBigNumber = '9223372036854775807';
|
const expectedBigNumber = '9223372036854775807';
|
||||||
@@ -160,7 +162,7 @@ describe('chart actions', () => {
|
|||||||
expect(json.value.toString()).toEqual(expectedBigNumber);
|
expect(json.value.toString()).toEqual(expectedBigNumber);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleChartDataResponse should return result if GlobalAsyncQueries flag is disabled', async () => {
|
test('handleChartDataResponse should return result if GlobalAsyncQueries flag is disabled', async () => {
|
||||||
const result = await handleChartDataResponse(
|
const result = await handleChartDataResponse(
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
{ result: [1, 2, 3] },
|
{ result: [1, 2, 3] },
|
||||||
@@ -168,7 +170,7 @@ describe('chart actions', () => {
|
|||||||
expect(result).toEqual([1, 2, 3]);
|
expect(result).toEqual([1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and results are returned synchronously', async () => {
|
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and results are returned synchronously', async () => {
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.GlobalAsyncQueries]: true,
|
[FeatureFlag.GlobalAsyncQueries]: true,
|
||||||
};
|
};
|
||||||
@@ -179,7 +181,7 @@ describe('chart actions', () => {
|
|||||||
expect(result).toEqual([1, 2, 3]);
|
expect(result).toEqual([1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and query is running asynchronously', async () => {
|
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and query is running asynchronously', async () => {
|
||||||
global.featureFlags = {
|
global.featureFlags = {
|
||||||
[FeatureFlag.GlobalAsyncQueries]: true,
|
[FeatureFlag.GlobalAsyncQueries]: true,
|
||||||
};
|
};
|
||||||
@@ -191,12 +193,13 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('legacy API', () => {
|
describe('legacy API', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fakeMetadata = { useLegacyApi: true };
|
fakeMetadata = { useLegacyApi: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
|
test('should dispatch CHART_UPDATE_STARTED action before the query', () => {
|
||||||
const actionThunk = actions.postChartFormData({});
|
const actionThunk = actions.postChartFormData({});
|
||||||
|
|
||||||
return actionThunk(dispatch, mockGetState).then(() => {
|
return actionThunk(dispatch, mockGetState).then(() => {
|
||||||
@@ -207,7 +210,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch TRIGGER_QUERY action with the query', () => {
|
test('should dispatch TRIGGER_QUERY action with the query', () => {
|
||||||
const actionThunk = actions.postChartFormData({});
|
const actionThunk = actions.postChartFormData({});
|
||||||
return actionThunk(dispatch, mockGetState).then(() => {
|
return actionThunk(dispatch, mockGetState).then(() => {
|
||||||
// chart update, trigger query, update form data, success
|
// chart update, trigger query, update form data, success
|
||||||
@@ -217,7 +220,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
|
test('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
|
||||||
const actionThunk = actions.postChartFormData({});
|
const actionThunk = actions.postChartFormData({});
|
||||||
return actionThunk(dispatch, mockGetState).then(() => {
|
return actionThunk(dispatch, mockGetState).then(() => {
|
||||||
// chart update, trigger query, update form data, success
|
// chart update, trigger query, update form data, success
|
||||||
@@ -227,7 +230,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch logEvent async action', () => {
|
test('should dispatch logEvent async action', () => {
|
||||||
const actionThunk = actions.postChartFormData({});
|
const actionThunk = actions.postChartFormData({});
|
||||||
return actionThunk(dispatch, mockGetState).then(() => {
|
return actionThunk(dispatch, mockGetState).then(() => {
|
||||||
// chart update, trigger query, update form data, success
|
// chart update, trigger query, update form data, success
|
||||||
@@ -241,7 +244,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
|
test('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
|
||||||
const actionThunk = actions.postChartFormData({});
|
const actionThunk = actions.postChartFormData({});
|
||||||
return actionThunk(dispatch, mockGetState).then(() => {
|
return actionThunk(dispatch, mockGetState).then(() => {
|
||||||
// chart update, trigger query, update form data, success
|
// chart update, trigger query, update form data, success
|
||||||
@@ -251,7 +254,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
|
test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
|
||||||
const unresolvingPromise = new Promise(() => {});
|
const unresolvingPromise = new Promise(() => {});
|
||||||
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
|
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
|
||||||
overwriteRoutes: true,
|
overwriteRoutes: true,
|
||||||
@@ -269,7 +272,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
|
test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
|
||||||
fetchMock.post(
|
fetchMock.post(
|
||||||
MOCK_URL,
|
MOCK_URL,
|
||||||
{ throws: { statusText: 'misc error' } },
|
{ throws: { statusText: 'misc error' } },
|
||||||
@@ -290,7 +293,7 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle the bigint without regression', async () => {
|
test('should handle the bigint without regression', async () => {
|
||||||
getExploreUrlStub.restore();
|
getExploreUrlStub.restore();
|
||||||
const mockBigIntUrl = '/mock/chart/data/bigint';
|
const mockBigIntUrl = '/mock/chart/data/bigint';
|
||||||
const expectedBigNumber = '9223372036854775807';
|
const expectedBigNumber = '9223372036854775807';
|
||||||
@@ -310,13 +313,14 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('runAnnotationQuery', () => {
|
describe('runAnnotationQuery', () => {
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
|
test('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
|
||||||
const annotation = {
|
const annotation = {
|
||||||
name: 'Holidays',
|
name: 'Holidays',
|
||||||
annotationType: 'EVENT',
|
annotationType: 'EVENT',
|
||||||
@@ -366,12 +370,13 @@ describe('chart actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('chart actions timeout', () => {
|
describe('chart actions timeout', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the timeout from arguments when given', async () => {
|
test('should use the timeout from arguments when given', async () => {
|
||||||
const postSpy = jest.spyOn(SupersetClient, 'post');
|
const postSpy = jest.spyOn(SupersetClient, 'post');
|
||||||
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
|
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
|
||||||
const timeout = 10; // Set the timeout value here
|
const timeout = 10; // Set the timeout value here
|
||||||
@@ -403,7 +408,7 @@ describe('chart actions timeout', () => {
|
|||||||
expect(postSpy).toHaveBeenCalledWith(expectedPayload);
|
expect(postSpy).toHaveBeenCalledWith(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the timeout from common.conf when not passed as an argument', async () => {
|
test('should use the timeout from common.conf when not passed as an argument', async () => {
|
||||||
const postSpy = jest.spyOn(SupersetClient, 'post');
|
const postSpy = jest.spyOn(SupersetClient, 'post');
|
||||||
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
|
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
|
||||||
const formData = { datasource: 'table__1' }; // Set the formData here
|
const formData = { datasource: 'table__1' }; // Set the formData here
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import chartReducer, { chart } from 'src/components/Chart/chartReducer';
|
import chartReducer, { chart } from 'src/components/Chart/chartReducer';
|
||||||
import * as actions from 'src/components/Chart/chartAction';
|
import * as actions from 'src/components/Chart/chartAction';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('chart reducers', () => {
|
describe('chart reducers', () => {
|
||||||
const chartKey = 1;
|
const chartKey = 1;
|
||||||
let testChart;
|
let testChart;
|
||||||
@@ -31,13 +32,13 @@ describe('chart reducers', () => {
|
|||||||
charts = { [chartKey]: testChart };
|
charts = { [chartKey]: testChart };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update endtime on fail', () => {
|
test('should update endtime on fail', () => {
|
||||||
const newState = chartReducer(charts, actions.chartUpdateStopped(chartKey));
|
const newState = chartReducer(charts, actions.chartUpdateStopped(chartKey));
|
||||||
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
|
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
|
||||||
expect(newState[chartKey].chartStatus).toEqual('stopped');
|
expect(newState[chartKey].chartStatus).toEqual('stopped');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update endtime on timeout', () => {
|
test('should update endtime on timeout', () => {
|
||||||
const newState = chartReducer(
|
const newState = chartReducer(
|
||||||
charts,
|
charts,
|
||||||
actions.chartUpdateFailed(
|
actions.chartUpdateFailed(
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ import {
|
|||||||
t,
|
t,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { MenuItem } from '@superset-ui/core/components/Menu';
|
import { type ItemType } from '@superset-ui/core/components/Menu';
|
||||||
import { RootState } from 'src/dashboard/types';
|
import { RootState } from 'src/dashboard/types';
|
||||||
import { getSubmenuYOffset } from '../utils';
|
import { getSubmenuYOffset } from '../utils';
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
import { useMenuItemWithTruncation } from '../MenuItemWithTruncation';
|
import { TruncatedMenuLabel } from '../MenuItemWithTruncation';
|
||||||
|
import { Dataset } from '../types';
|
||||||
|
|
||||||
const DRILL_TO_DETAIL = t('Drill to detail');
|
const DRILL_TO_DETAIL = t('Drill to detail');
|
||||||
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
|
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
|
||||||
@@ -60,28 +61,6 @@ const DISABLED_REASONS = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDisabledMenuItem(
|
|
||||||
children: ReactNode,
|
|
||||||
menuKey: string,
|
|
||||||
...rest: unknown[]
|
|
||||||
): MenuItem {
|
|
||||||
return {
|
|
||||||
disabled: true,
|
|
||||||
key: menuKey,
|
|
||||||
label: (
|
|
||||||
<div
|
|
||||||
css={css`
|
|
||||||
white-space: normal;
|
|
||||||
max-width: 160px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Filter = ({
|
const Filter = ({
|
||||||
children,
|
children,
|
||||||
stripHTML = false,
|
stripHTML = false,
|
||||||
@@ -103,7 +82,7 @@ const StyledFilter = styled(Filter)`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type DrillDetailMenuItemsArgs = {
|
export type DrillDetailMenuItemsProps = {
|
||||||
formData: QueryFormData;
|
formData: QueryFormData;
|
||||||
filters?: BinaryQueryObjectFilterClause[];
|
filters?: BinaryQueryObjectFilterClause[];
|
||||||
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
|
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
|
||||||
@@ -115,6 +94,8 @@ export type DrillDetailMenuItemsArgs = {
|
|||||||
setShowModal: (show: boolean) => void;
|
setShowModal: (show: boolean) => void;
|
||||||
key?: string;
|
key?: string;
|
||||||
forceSubmenuRender?: boolean;
|
forceSubmenuRender?: boolean;
|
||||||
|
dataset?: Dataset;
|
||||||
|
isLoadingDataset?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDrillDetailMenuItems = ({
|
export const useDrillDetailMenuItems = ({
|
||||||
@@ -129,7 +110,7 @@ export const useDrillDetailMenuItems = ({
|
|||||||
setShowModal,
|
setShowModal,
|
||||||
key,
|
key,
|
||||||
...props
|
...props
|
||||||
}: DrillDetailMenuItemsArgs) => {
|
}: DrillDetailMenuItemsProps): ItemType[] => {
|
||||||
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
|
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
|
||||||
({ datasources }) =>
|
({ datasources }) =>
|
||||||
datasources[formData.datasource]?.database?.disable_drill_to_detail,
|
datasources[formData.datasource]?.database?.disable_drill_to_detail,
|
||||||
@@ -142,7 +123,7 @@ export const useDrillDetailMenuItems = ({
|
|||||||
setFilters(filters);
|
setFilters(filters);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
},
|
},
|
||||||
[onClick, onSelection],
|
[onClick, onSelection, setFilters, setShowModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu`
|
// Check for Behavior.DRILL_TO_DETAIL to tell if plugin handles the `contextmenu`
|
||||||
@@ -191,79 +172,110 @@ export const useDrillDetailMenuItems = ({
|
|||||||
drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
|
drillByDisabled = DISABLED_REASONS.NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const drillToDetailMenuItem: MenuItem = drillDisabled
|
const drillToDetailMenuItem: ItemType = drillDisabled
|
||||||
? getDisabledMenuItem(
|
? {
|
||||||
<>
|
key: 'drill-to-detail-disabled',
|
||||||
{DRILL_TO_DETAIL}
|
disabled: true,
|
||||||
<MenuItemTooltip title={drillDisabled} />
|
label: (
|
||||||
</>,
|
<div
|
||||||
'drill-to-detail-disabled',
|
css={css`
|
||||||
props,
|
white-space: normal;
|
||||||
)
|
max-width: 160px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{DRILL_TO_DETAIL}
|
||||||
|
<MenuItemTooltip title={drillDisabled} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
...props,
|
||||||
|
}
|
||||||
: {
|
: {
|
||||||
key: 'drill-to-detail',
|
key: 'drill-to-detail',
|
||||||
label: DRILL_TO_DETAIL,
|
|
||||||
onClick: openModal.bind(null, []),
|
onClick: openModal.bind(null, []),
|
||||||
...props,
|
label: DRILL_TO_DETAIL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMenuItemWithTruncation = useMenuItemWithTruncation();
|
const drillToDetailByMenuItem: ItemType | null = !isContextMenu
|
||||||
|
? null
|
||||||
const drillToDetailByMenuItem: MenuItem = drillByDisabled
|
: drillByDisabled
|
||||||
? getDisabledMenuItem(
|
? {
|
||||||
<>
|
key: 'drill-to-detail-by-disabled',
|
||||||
{DRILL_TO_DETAIL_BY}
|
disabled: true,
|
||||||
<MenuItemTooltip title={drillByDisabled} />
|
label: (
|
||||||
</>,
|
<div
|
||||||
'drill-to-detail-by-disabled',
|
css={css`
|
||||||
props,
|
white-space: normal;
|
||||||
)
|
max-width: 160px;
|
||||||
: {
|
`}
|
||||||
key: key || 'drill-to-detail-by',
|
>
|
||||||
label: DRILL_TO_DETAIL_BY,
|
{DRILL_TO_DETAIL_BY}
|
||||||
children: [
|
<MenuItemTooltip title={drillByDisabled} />
|
||||||
...filters.map((filter, i) => ({
|
</div>
|
||||||
key: `drill-detail-filter-${i}`,
|
),
|
||||||
label: getMenuItemWithTruncation({
|
...props,
|
||||||
tooltipText: `${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`,
|
}
|
||||||
onClick: openModal.bind(null, [filter]),
|
: {
|
||||||
|
key: key || 'drill-to-detail-by',
|
||||||
|
label: DRILL_TO_DETAIL_BY,
|
||||||
|
popupOffset: [0, submenuYOffset],
|
||||||
|
popupClassName: 'chart-context-submenu',
|
||||||
|
children: [
|
||||||
|
...filters.map((filter, i) => ({
|
||||||
key: `drill-detail-filter-${i}`,
|
key: `drill-detail-filter-${i}`,
|
||||||
children: (
|
onClick: openModal.bind(null, [filter]),
|
||||||
<>
|
label: (
|
||||||
{`${DRILL_TO_DETAIL_BY} `}
|
<div
|
||||||
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
|
css={css`
|
||||||
</>
|
max-width: 200px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<TruncatedMenuLabel
|
||||||
|
tooltipText={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
||||||
|
aria-label={`${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
css={css`
|
||||||
|
display: inline;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{DRILL_TO_DETAIL_BY}{' '}
|
||||||
|
<StyledFilter stripHTML>
|
||||||
|
{filter.formattedVal}
|
||||||
|
</StyledFilter>
|
||||||
|
</span>
|
||||||
|
</TruncatedMenuLabel>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
}),
|
})),
|
||||||
})),
|
...(filters.length > 1
|
||||||
filters.length > 1 && {
|
? [
|
||||||
key: 'drill-detail-filter-all',
|
{
|
||||||
label: getMenuItemWithTruncation({
|
key: 'drill-detail-filter-all',
|
||||||
tooltipText: `${DRILL_TO_DETAIL_BY} ${t('all')}`,
|
onClick: openModal.bind(null, filters),
|
||||||
onClick: openModal.bind(null, filters),
|
label: (
|
||||||
key: 'drill-detail-filter-all',
|
<div
|
||||||
children: (
|
aria-label={`${DRILL_TO_DETAIL_BY} ${t('all')}`}
|
||||||
<>
|
css={css`
|
||||||
{`${DRILL_TO_DETAIL_BY} `}
|
max-width: 200px;
|
||||||
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
|
`}
|
||||||
</>
|
>
|
||||||
),
|
{`${DRILL_TO_DETAIL_BY} `}
|
||||||
}),
|
<StyledFilter stripHTML={false}>
|
||||||
},
|
{t('all')}
|
||||||
].filter(Boolean) as MenuItem[],
|
</StyledFilter>
|
||||||
onClick: openModal.bind(null, filters),
|
</div>
|
||||||
forceSubmenuRender: true,
|
),
|
||||||
popupOffset: [0, submenuYOffset],
|
},
|
||||||
popupClassName: 'chart-context-submenu',
|
]
|
||||||
...props,
|
: []),
|
||||||
};
|
],
|
||||||
if (isContextMenu) {
|
...props,
|
||||||
return {
|
};
|
||||||
drillToDetailMenuItem,
|
|
||||||
drillToDetailByMenuItem,
|
const menuItems: ItemType[] = [drillToDetailMenuItem];
|
||||||
};
|
if (drillToDetailByMenuItem) {
|
||||||
|
menuItems.push(drillToDetailByMenuItem);
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
drillToDetailMenuItem,
|
return menuItems;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
render,
|
render,
|
||||||
screen,
|
screen,
|
||||||
userEvent,
|
userEvent,
|
||||||
|
waitFor,
|
||||||
within,
|
within,
|
||||||
} from 'spec/helpers/testing-library';
|
} from 'spec/helpers/testing-library';
|
||||||
import setupPlugins from 'src/setup/setupPlugins';
|
import setupPlugins from 'src/setup/setupPlugins';
|
||||||
@@ -29,15 +30,13 @@ import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
|
|||||||
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
|
||||||
import { BinaryQueryObjectFilterClause, VizType } from '@superset-ui/core';
|
import { BinaryQueryObjectFilterClause, VizType } from '@superset-ui/core';
|
||||||
import { Menu } from '@superset-ui/core/components/Menu';
|
import { Menu } from '@superset-ui/core/components/Menu';
|
||||||
import DrillDetailMenuItems, {
|
import DrillDetailModal from '../DrillDetail/DrillDetailModal';
|
||||||
DrillDetailMenuItemsProps,
|
import { useDrillDetailMenuItems, DrillDetailMenuItemsProps } from './index';
|
||||||
} from './DrillDetailMenuItems';
|
|
||||||
import DrillDetailModal from './DrillDetailModal';
|
|
||||||
|
|
||||||
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
|
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./DrillDetailPane',
|
'../DrillDetail/DrillDetailPane',
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
initialFilters,
|
initialFilters,
|
||||||
@@ -87,17 +86,17 @@ const MockRenderChart = ({
|
|||||||
BinaryQueryObjectFilterClause[] | undefined
|
BinaryQueryObjectFilterClause[] | undefined
|
||||||
>(filters);
|
>(filters);
|
||||||
|
|
||||||
|
const menuItems = useDrillDetailMenuItems({
|
||||||
|
setFilters,
|
||||||
|
formData: formData ?? defaultFormData,
|
||||||
|
filters: modalFilters,
|
||||||
|
isContextMenu,
|
||||||
|
setShowModal: setShowMenu,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu forceSubMenuRender>
|
<Menu forceSubMenuRender items={menuItems} />
|
||||||
<DrillDetailMenuItems
|
|
||||||
setFilters={setFilters}
|
|
||||||
formData={formData ?? defaultFormData}
|
|
||||||
filters={modalFilters}
|
|
||||||
isContextMenu={isContextMenu}
|
|
||||||
setShowModal={setShowMenu}
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<DrillDetailModal
|
<DrillDetailModal
|
||||||
chartId={chartId ?? defaultChartId}
|
chartId={chartId ?? defaultChartId}
|
||||||
@@ -130,8 +129,10 @@ const renderMenu = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupMenu = (filters: BinaryQueryObjectFilterClause[]) => {
|
const setupMenu = async (filters: BinaryQueryObjectFilterClause[]) => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
// Small delay to ensure DOM cleanup is complete
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
renderMenu({
|
renderMenu({
|
||||||
chartId: defaultChartId,
|
chartId: defaultChartId,
|
||||||
formData: defaultFormData,
|
formData: defaultFormData,
|
||||||
@@ -235,11 +236,11 @@ const expectDrillToDetailByEnabled = async () => {
|
|||||||
.getAllByRole('menuitem')
|
.getAllByRole('menuitem')
|
||||||
.find(menuItem => within(menuItem).queryByText('Drill to detail by'));
|
.find(menuItem => within(menuItem).queryByText('Drill to detail by'));
|
||||||
await expectMenuItemEnabled(drillToDetailBy!);
|
await expectMenuItemEnabled(drillToDetailBy!);
|
||||||
|
|
||||||
userEvent.hover(drillToDetailBy!);
|
userEvent.hover(drillToDetailBy!);
|
||||||
|
|
||||||
const submenus = await screen.findAllByTestId('drill-to-detail-by-submenu');
|
const submenu = await screen.findByRole('menu', {});
|
||||||
|
expect(submenu).toBeInTheDocument();
|
||||||
expect(submenus.length).toEqual(2);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,17 +259,37 @@ const expectDrillToDetailByDisabled = async (tooltipContent?: string) => {
|
|||||||
const expectDrillToDetailByDimension = async (
|
const expectDrillToDetailByDimension = async (
|
||||||
filter: BinaryQueryObjectFilterClause,
|
filter: BinaryQueryObjectFilterClause,
|
||||||
) => {
|
) => {
|
||||||
userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' }));
|
const formattedVal = filter.formattedVal as string;
|
||||||
const drillToDetailBySubMenus = await screen.findAllByTestId(
|
const drillByMenuItem = screen.getByRole('menuitem', {
|
||||||
'drill-to-detail-by-submenu',
|
name: 'Drill to detail by',
|
||||||
);
|
});
|
||||||
|
|
||||||
|
userEvent.hover(drillByMenuItem);
|
||||||
|
|
||||||
|
const submenuPopup = (await waitFor(() =>
|
||||||
|
screen
|
||||||
|
.getAllByRole('menu', { hidden: true })
|
||||||
|
.find(menu =>
|
||||||
|
(menu.textContent ?? '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.includes(formattedVal),
|
||||||
|
),
|
||||||
|
)) as HTMLElement;
|
||||||
|
|
||||||
|
const drillToDetailBySubmenuItem = (await waitFor(() => {
|
||||||
|
const items = within(submenuPopup).getAllByRole('menuitem');
|
||||||
|
return items.find(item =>
|
||||||
|
(item.textContent ?? '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.includes(`Drill to detail by ${filter.formattedVal}`),
|
||||||
|
);
|
||||||
|
})) as HTMLElement;
|
||||||
|
|
||||||
const menuItemName = `Drill to detail by ${filter.formattedVal}`;
|
const menuItemName = `Drill to detail by ${filter.formattedVal}`;
|
||||||
const drillToDetailBySubmenuItems = await within(
|
|
||||||
drillToDetailBySubMenus[1],
|
|
||||||
).findAllByRole('menuitem');
|
|
||||||
|
|
||||||
await expectMenuItemEnabled(drillToDetailBySubmenuItems[0]);
|
await expectMenuItemEnabled(drillToDetailBySubmenuItem);
|
||||||
await expectDrillToDetailModal(menuItemName, [filter]);
|
await expectDrillToDetailModal(menuItemName, [filter]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,14 +299,15 @@ const expectDrillToDetailByDimension = async (
|
|||||||
const expectDrillToDetailByAll = async (
|
const expectDrillToDetailByAll = async (
|
||||||
filters: BinaryQueryObjectFilterClause[],
|
filters: BinaryQueryObjectFilterClause[],
|
||||||
) => {
|
) => {
|
||||||
userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' }));
|
const drillByMenuItem = screen.getByRole('menuitem', {
|
||||||
const drillToDetailBySubMenus = await screen.findAllByTestId(
|
name: 'Drill to detail by',
|
||||||
'drill-to-detail-by-submenu',
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const drillToDetailBySubmenuItem = await within(
|
userEvent.hover(drillByMenuItem);
|
||||||
drillToDetailBySubMenus[1],
|
|
||||||
).findByText(/all/i);
|
await screen.findByRole('menu');
|
||||||
|
|
||||||
|
const drillToDetailBySubmenuItem = await screen.findByText(/all/i, {});
|
||||||
|
|
||||||
await expectMenuItemEnabled(drillToDetailBySubmenuItem);
|
await expectMenuItemEnabled(drillToDetailBySubmenuItem);
|
||||||
|
|
||||||
@@ -386,20 +408,20 @@ test('context menu for supported chart, dimensions, 1 filter', async () => {
|
|||||||
|
|
||||||
test('context menu for supported chart, dimensions, filter A', async () => {
|
test('context menu for supported chart, dimensions, filter A', async () => {
|
||||||
const filters = [filterA, filterB];
|
const filters = [filterA, filterB];
|
||||||
setupMenu(filters);
|
await setupMenu(filters);
|
||||||
await expectDrillToDetailByEnabled();
|
await expectDrillToDetailByEnabled();
|
||||||
await expectDrillToDetailByDimension(filterA);
|
await expectDrillToDetailByDimension(filterA);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('context menu for supported chart, dimensions, filter B', async () => {
|
test('context menu for supported chart, dimensions, filter B', async () => {
|
||||||
const filters = [filterA, filterB];
|
const filters = [filterA, filterB];
|
||||||
setupMenu(filters);
|
await setupMenu(filters);
|
||||||
await expectDrillToDetailByEnabled();
|
await expectDrillToDetailByEnabled();
|
||||||
await expectDrillToDetailByDimension(filterB);
|
await expectDrillToDetailByDimension(filterB);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('context menu for supported chart, dimensions, all filters', async () => {
|
test.skip('context menu for supported chart, dimensions, all filters', async () => {
|
||||||
const filters = [filterA, filterB];
|
const filters = [filterA, filterB];
|
||||||
setupMenu(filters);
|
await setupMenu(filters);
|
||||||
await expectDrillToDetailByAll(filters);
|
await expectDrillToDetailByAll(filters);
|
||||||
});
|
});
|
||||||
@@ -73,24 +73,25 @@ beforeEach(() => {
|
|||||||
fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] });
|
fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceModal', () => {
|
describe('DatasourceModal', () => {
|
||||||
it('renders', async () => {
|
test('renders', async () => {
|
||||||
expect(container).toBeDefined();
|
expect(container).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the component', () => {
|
test('renders the component', () => {
|
||||||
expect(screen.getByText('Edit Dataset')).toBeInTheDocument();
|
expect(screen.getByText('Edit Dataset')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Modal', async () => {
|
test('renders a Modal', async () => {
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a DatasourceEditor', async () => {
|
test('renders a DatasourceEditor', async () => {
|
||||||
expect(screen.getByTestId('datasource-editor')).toBeInTheDocument();
|
expect(screen.getByTestId('datasource-editor')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables the save button when the datasource is managed externally', () => {
|
test('disables the save button when the datasource is managed externally', () => {
|
||||||
// the render is currently in a before operation, so it needs to be cleaned up
|
// the render is currently in a before operation, so it needs to be cleaned up
|
||||||
// we could alternatively move all the renders back into the tests or find a better
|
// we could alternatively move all the renders back into the tests or find a better
|
||||||
// way to automatically render but still allow to pass in props with the tests
|
// way to automatically render but still allow to pass in props with the tests
|
||||||
@@ -104,7 +105,7 @@ describe('DatasourceModal', () => {
|
|||||||
expect(saveButton).toBeDisabled();
|
expect(saveButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onDatasourceSave function when the save button is clicked', async () => {
|
test('calls the onDatasourceSave function when the save button is clicked', async () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
const onDatasourceSave = jest.fn();
|
const onDatasourceSave = jest.fn();
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ describe('DatasourceModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render error dialog', async () => {
|
test('should render error dialog', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(SupersetClient, 'put')
|
.spyOn(SupersetClient, 'put')
|
||||||
.mockRejectedValue(new Error('Something went wrong'));
|
.mockRejectedValue(new Error('Something went wrong'));
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
fireEvent,
|
||||||
|
act,
|
||||||
|
defaultStore as store,
|
||||||
|
} from 'spec/helpers/testing-library';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import { Modal } from '@superset-ui/core/components';
|
||||||
|
import mockDatasource from 'spec/fixtures/mockDatasource';
|
||||||
|
import DatasourceModal from '.';
|
||||||
|
|
||||||
|
const mockedProps = {
|
||||||
|
datasource: mockDatasource['7__table'],
|
||||||
|
addSuccessToast: jest.fn(),
|
||||||
|
addDangerToast: jest.fn(),
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onHide: jest.fn(),
|
||||||
|
show: true,
|
||||||
|
onDatasourceSave: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.put('glob:*/api/v1/dataset/7?override_columns=*', {});
|
||||||
|
fetchMock.get('glob:*/api/v1/dataset/7', { result: {} });
|
||||||
|
fetchMock.get('glob:*/api/v1/database/?q=*', { result: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatasourceModal - should use Modal.useModal hook instead of Modal.confirm directly', () => {
|
||||||
|
const useModalSpy = jest.spyOn(Modal, 'useModal');
|
||||||
|
const confirmSpy = jest.spyOn(Modal, 'confirm');
|
||||||
|
|
||||||
|
render(<DatasourceModal {...mockedProps} />, { store });
|
||||||
|
|
||||||
|
// Should use the useModal hook
|
||||||
|
expect(useModalSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Should not call Modal.confirm during initial render
|
||||||
|
expect(confirmSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
useModalSpy.mockRestore();
|
||||||
|
confirmSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatasourceModal - should handle sync columns state without imperative modal updates', async () => {
|
||||||
|
// Test that we can successfully click the save button without DOM errors
|
||||||
|
// The actual checkbox is only visible when SQL has changed
|
||||||
|
render(<DatasourceModal {...mockedProps} />, { store });
|
||||||
|
|
||||||
|
const saveButton = screen.getByTestId('datasource-modal-save');
|
||||||
|
|
||||||
|
// This should not throw any DOM errors
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(saveButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show confirmation modal
|
||||||
|
expect(screen.getByText('Confirm save')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Should show the confirmation message
|
||||||
|
expect(
|
||||||
|
screen.getByText('Are you sure you want to save and apply changes?'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DatasourceModal - should not store modal instance in state', () => {
|
||||||
|
// Mock console.warn to catch any warnings about refs or imperatives
|
||||||
|
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
|
render(<DatasourceModal {...mockedProps} />, { store });
|
||||||
|
|
||||||
|
// No warnings should be generated about improper React patterns
|
||||||
|
const reactWarnings = consoleWarn.mock.calls.filter(call =>
|
||||||
|
call.some(
|
||||||
|
arg =>
|
||||||
|
typeof arg === 'string' &&
|
||||||
|
(arg.includes('findDOMNode') ||
|
||||||
|
arg.includes('ref') ||
|
||||||
|
arg.includes('instance')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reactWarnings).toHaveLength(0);
|
||||||
|
|
||||||
|
consoleWarn.mockRestore();
|
||||||
|
});
|
||||||
@@ -16,13 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { FunctionComponent, useState, useEffect, useCallback } from 'react';
|
||||||
FunctionComponent,
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
} from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
styled,
|
styled,
|
||||||
@@ -101,8 +95,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [currentDatasource, setCurrentDatasource] = useState(datasource);
|
const [currentDatasource, setCurrentDatasource] = useState(datasource);
|
||||||
const syncColumnsRef = useRef(false);
|
const [syncColumns, setSyncColumns] = useState(false);
|
||||||
const [confirmModal, setConfirmModal] = useState<any>(null);
|
|
||||||
const currencies = useSelector<
|
const currencies = useSelector<
|
||||||
{
|
{
|
||||||
common: {
|
common: {
|
||||||
@@ -114,7 +107,6 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
const [errors, setErrors] = useState<any[]>([]);
|
const [errors, setErrors] = useState<any[]>([]);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const dialog = useRef<any>(null);
|
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const buildPayload = (datasource: Record<string, any>) => {
|
const buildPayload = (datasource: Record<string, any>) => {
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
@@ -196,7 +188,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await SupersetClient.put({
|
await SupersetClient.put({
|
||||||
endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${syncColumnsRef.current}`,
|
endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${syncColumns}`,
|
||||||
jsonPayload: buildPayload(currentDatasource),
|
jsonPayload: buildPayload(currentDatasource),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -281,14 +273,9 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
impact the column definitions, you might want to skip this step.`)}
|
impact the column definitions, you might want to skip this step.`)}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={syncColumnsRef.current}
|
checked={syncColumns}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
syncColumnsRef.current = !syncColumnsRef.current;
|
setSyncColumns(!syncColumns);
|
||||||
if (confirmModal) {
|
|
||||||
confirmModal.update({
|
|
||||||
content: getSaveDialog(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
@@ -303,25 +290,17 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
{t('Are you sure you want to save and apply changes?')}
|
{t('Are you sure you want to save and apply changes?')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[currentDatasource.sql, datasource.sql, confirmModal],
|
[currentDatasource.sql, datasource.sql, syncColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (confirmModal) {
|
|
||||||
confirmModal.update({
|
|
||||||
content: getSaveDialog(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [confirmModal, getSaveDialog]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (datasource.sql !== currentDatasource.sql) {
|
if (datasource.sql !== currentDatasource.sql) {
|
||||||
syncColumnsRef.current = true;
|
setSyncColumns(true);
|
||||||
}
|
}
|
||||||
}, [datasource.sql, currentDatasource.sql]);
|
}, [datasource.sql, currentDatasource.sql]);
|
||||||
|
|
||||||
const onClickSave = () => {
|
const onClickSave = () => {
|
||||||
const modalInstance = modal.confirm({
|
modal.confirm({
|
||||||
title: t('Confirm save'),
|
title: t('Confirm save'),
|
||||||
content: getSaveDialog(),
|
content: getSaveDialog(),
|
||||||
onOk: onConfirmSave,
|
onOk: onConfirmSave,
|
||||||
@@ -329,8 +308,6 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
|||||||
okText: t('OK'),
|
okText: t('OK'),
|
||||||
cancelText: t('Cancel'),
|
cancelText: t('Cancel'),
|
||||||
});
|
});
|
||||||
setConfirmModal(modalInstance);
|
|
||||||
dialog.current = modalInstance;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -104,10 +104,10 @@ export default class CRUDCollection extends PureComponent<
|
|||||||
this.toggleExpand = this.toggleExpand.bind(this);
|
this.toggleExpand = this.toggleExpand.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps: CRUDCollectionProps) {
|
componentDidUpdate(prevProps: CRUDCollectionProps) {
|
||||||
if (nextProps.collection !== this.props.collection) {
|
if (this.props.collection !== prevProps.collection) {
|
||||||
const { collection, collectionArray } = createKeyedCollection(
|
const { collection, collectionArray } = createKeyedCollection(
|
||||||
nextProps.collection,
|
this.props.collection,
|
||||||
);
|
);
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
collection,
|
collection,
|
||||||
|
|||||||
@@ -740,7 +740,6 @@ class DatasourceEditor extends PureComponent {
|
|||||||
this.props.runQuery({
|
this.props.runQuery({
|
||||||
client_id: this.props.clientId,
|
client_id: this.props.clientId,
|
||||||
database_id: this.state.datasource.database.id,
|
database_id: this.state.datasource.database.id,
|
||||||
json: true,
|
|
||||||
runAsync: false,
|
runAsync: false,
|
||||||
catalog: this.state.datasource.catalog,
|
catalog: this.state.datasource.catalog,
|
||||||
schema: this.state.datasource.schema,
|
schema: this.state.datasource.schema,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const setupTest = (dashboards = mockDashboards) =>
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DashboardLinksExternal', () => {
|
describe('DashboardLinksExternal', () => {
|
||||||
test('renders empty state when no dashboards provided', () => {
|
test('renders empty state when no dashboards provided', () => {
|
||||||
setupTest([]);
|
setupTest([]);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const asyncRender = props =>
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceEditor', () => {
|
describe('DatasourceEditor', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -80,11 +81,11 @@ describe('DatasourceEditor', () => {
|
|||||||
// jest.clearAllMocks();
|
// jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Tabs', () => {
|
test('renders Tabs', () => {
|
||||||
expect(screen.getByTestId('edit-dataset-tabs')).toBeInTheDocument();
|
expect(screen.getByTestId('edit-dataset-tabs')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can sync columns from source', async () => {
|
test('can sync columns from source', async () => {
|
||||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||||
userEvent.click(columnsTab);
|
userEvent.click(columnsTab);
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ describe('DatasourceEditor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// to add, remove and modify columns accordingly
|
// to add, remove and modify columns accordingly
|
||||||
it('can modify columns', async () => {
|
test('can modify columns', async () => {
|
||||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||||
userEvent.click(columnsTab);
|
userEvent.click(columnsTab);
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ describe('DatasourceEditor', () => {
|
|||||||
userEvent.type(inputCertDetails, 'test');
|
userEvent.type(inputCertDetails, 'test');
|
||||||
}, 40000);
|
}, 40000);
|
||||||
|
|
||||||
it('can delete columns', async () => {
|
test('can delete columns', async () => {
|
||||||
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
const columnsTab = screen.getByTestId('collection-tab-Columns');
|
||||||
userEvent.click(columnsTab);
|
userEvent.click(columnsTab);
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ describe('DatasourceEditor', () => {
|
|||||||
});
|
});
|
||||||
}, 60000); // 60 seconds timeout to avoid timeouts
|
}, 60000); // 60 seconds timeout to avoid timeouts
|
||||||
|
|
||||||
it('can add new columns', async () => {
|
test('can add new columns', async () => {
|
||||||
const calcColsTab = screen.getByTestId('collection-tab-Calculated columns');
|
const calcColsTab = screen.getByTestId('collection-tab-Calculated columns');
|
||||||
userEvent.click(calcColsTab);
|
userEvent.click(calcColsTab);
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ describe('DatasourceEditor', () => {
|
|||||||
});
|
});
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
it('renders isSqla fields', async () => {
|
test('renders isSqla fields', async () => {
|
||||||
const columnsTab = screen.getByRole('tab', {
|
const columnsTab = screen.getByRole('tab', {
|
||||||
name: /settings/i,
|
name: /settings/i,
|
||||||
});
|
});
|
||||||
@@ -195,6 +196,7 @@ describe('DatasourceEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceEditor Source Tab', () => {
|
describe('DatasourceEditor Source Tab', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
isFeatureEnabled.mockImplementation(() => false);
|
isFeatureEnabled.mockImplementation(() => false);
|
||||||
@@ -216,7 +218,7 @@ describe('DatasourceEditor Source Tab', () => {
|
|||||||
isFeatureEnabled.mockRestore();
|
isFeatureEnabled.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Source Tab: edit mode', async () => {
|
test('Source Tab: edit mode', async () => {
|
||||||
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
||||||
userEvent.click(getLockBtn);
|
userEvent.click(getLockBtn);
|
||||||
|
|
||||||
@@ -231,7 +233,7 @@ describe('DatasourceEditor Source Tab', () => {
|
|||||||
expect(virtualRadioBtn).toBeEnabled();
|
expect(virtualRadioBtn).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Source Tab: readOnly mode', () => {
|
test('Source Tab: readOnly mode', () => {
|
||||||
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
const getLockBtn = screen.getByRole('img', { name: /lock/i });
|
||||||
expect(getLockBtn).toBeInTheDocument();
|
expect(getLockBtn).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -246,7 +248,7 @@ describe('DatasourceEditor Source Tab', () => {
|
|||||||
expect(virtualRadioBtn).toBeDisabled();
|
expect(virtualRadioBtn).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onChange with empty SQL when switching to physical dataset', async () => {
|
test('calls onChange with empty SQL when switching to physical dataset', async () => {
|
||||||
// Clean previous render
|
// Clean previous render
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const fastRender = props =>
|
|||||||
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
|
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceEditor Currency Tests', () => {
|
describe('DatasourceEditor Currency Tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
||||||
@@ -40,7 +41,7 @@ describe('DatasourceEditor Currency Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The problematic test, now optimized
|
// The problematic test, now optimized
|
||||||
it('renders currency controls', async () => {
|
test('renders currency controls', async () => {
|
||||||
// Setup a metric with currency data
|
// Setup a metric with currency data
|
||||||
const propsWithCurrency = {
|
const propsWithCurrency = {
|
||||||
...props,
|
...props,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
DATASOURCE_ENDPOINT,
|
DATASOURCE_ENDPOINT,
|
||||||
} from './DatasourceEditor.test';
|
} from './DatasourceEditor.test';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceEditor RTL Metrics Tests', () => {
|
describe('DatasourceEditor RTL Metrics Tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
||||||
@@ -34,7 +35,7 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
|
|||||||
fetchMock.restore();
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly renders the metric information', async () => {
|
test('properly renders the metric information', async () => {
|
||||||
await asyncRender(props);
|
await asyncRender(props);
|
||||||
|
|
||||||
const metricButton = screen.getByTestId('collection-tab-Metrics');
|
const metricButton = screen.getByTestId('collection-tab-Metrics');
|
||||||
@@ -52,7 +53,7 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
|
|||||||
expect(warningMarkdown.value).toEqual('someone');
|
expect(warningMarkdown.value).toEqual('someone');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('properly updates the metric information', async () => {
|
test('properly updates the metric information', async () => {
|
||||||
await asyncRender(props);
|
await asyncRender(props);
|
||||||
|
|
||||||
const metricButton = screen.getByTestId('collection-tab-Metrics');
|
const metricButton = screen.getByTestId('collection-tab-Metrics');
|
||||||
@@ -82,6 +83,7 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DatasourceEditor RTL Columns Tests', () => {
|
describe('DatasourceEditor RTL Columns Tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
|
||||||
@@ -92,7 +94,7 @@ describe('DatasourceEditor RTL Columns Tests', () => {
|
|||||||
fetchMock.restore();
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the default datetime column', async () => {
|
test('shows the default datetime column', async () => {
|
||||||
await asyncRender(props);
|
await asyncRender(props);
|
||||||
|
|
||||||
const columnsButton = screen.getByTestId('collection-tab-Columns');
|
const columnsButton = screen.getByTestId('collection-tab-Columns');
|
||||||
@@ -107,7 +109,7 @@ describe('DatasourceEditor RTL Columns Tests', () => {
|
|||||||
expect(genderDefaultDatetimeRadio).not.toBeChecked();
|
expect(genderDefaultDatetimeRadio).not.toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows choosing only temporal columns as the default datetime', async () => {
|
test('allows choosing only temporal columns as the default datetime', async () => {
|
||||||
await asyncRender(props);
|
await asyncRender(props);
|
||||||
|
|
||||||
const columnsButton = screen.getByTestId('collection-tab-Columns');
|
const columnsButton = screen.getByTestId('collection-tab-Columns');
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { tn } from '@superset-ui/core';
|
import { tn } from '@superset-ui/core';
|
||||||
import { updateColumns } from '.';
|
import { updateColumns } from '.';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('updateColumns', () => {
|
describe('updateColumns', () => {
|
||||||
let addSuccessToast: jest.Mock;
|
let addSuccessToast: jest.Mock;
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ describe('updateColumns', () => {
|
|||||||
addSuccessToast = jest.fn();
|
addSuccessToast = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds new columns when prevCols is empty', () => {
|
test('adds new columns when prevCols is empty', () => {
|
||||||
interface Column {
|
interface Column {
|
||||||
column_name: string;
|
column_name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -56,7 +57,7 @@ describe('updateColumns', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('modifies columns when type or is_dttm changes', () => {
|
test('modifies columns when type or is_dttm changes', () => {
|
||||||
const prevCols = [
|
const prevCols = [
|
||||||
{ column_name: 'col1', type: 'string', is_dttm: false },
|
{ column_name: 'col1', type: 'string', is_dttm: false },
|
||||||
{ column_name: 'col2', type: 'number', is_dttm: false },
|
{ column_name: 'col2', type: 'number', is_dttm: false },
|
||||||
@@ -94,7 +95,7 @@ describe('updateColumns', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes columns not present in newCols', () => {
|
test('removes columns not present in newCols', () => {
|
||||||
const prevCols = [
|
const prevCols = [
|
||||||
{ column_name: 'col1', type: 'string', is_dttm: false },
|
{ column_name: 'col1', type: 'string', is_dttm: false },
|
||||||
{ column_name: 'col2', type: 'number', is_dttm: true },
|
{ column_name: 'col2', type: 'number', is_dttm: true },
|
||||||
@@ -121,7 +122,7 @@ describe('updateColumns', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles combined additions, modifications, and removals', () => {
|
test('handles combined additions, modifications, and removals', () => {
|
||||||
const prevCols = [
|
const prevCols = [
|
||||||
{ column_name: 'col1', type: 'string', is_dttm: false },
|
{ column_name: 'col1', type: 'string', is_dttm: false },
|
||||||
{ column_name: 'col2', type: 'number', is_dttm: false },
|
{ column_name: 'col2', type: 'number', is_dttm: false },
|
||||||
@@ -170,7 +171,7 @@ describe('updateColumns', () => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('should not remove columns with expressions', () => {
|
test('should not remove columns with expressions', () => {
|
||||||
const prevCols = [
|
const prevCols = [
|
||||||
{ column_name: 'col1', type: 'string', is_dttm: false },
|
{ column_name: 'col1', type: 'string', is_dttm: false },
|
||||||
{ column_name: 'col2', type: 'number', is_dttm: false },
|
{ column_name: 'col2', type: 'number', is_dttm: false },
|
||||||
|
|||||||
@@ -20,72 +20,71 @@
|
|||||||
import { screen, fireEvent, render } from 'spec/helpers/testing-library';
|
import { screen, fireEvent, render } from 'spec/helpers/testing-library';
|
||||||
import { ErrorAlert } from './ErrorAlert';
|
import { ErrorAlert } from './ErrorAlert';
|
||||||
|
|
||||||
describe('ErrorAlert', () => {
|
// ErrorAlert
|
||||||
it('renders the error message correctly', () => {
|
test('ErrorAlert renders the error message correctly', () => {
|
||||||
render(
|
render(
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
errorType="Error"
|
errorType="Error"
|
||||||
message="Something went wrong"
|
message="Something went wrong"
|
||||||
type="error"
|
type="error"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the description when provided', () => {
|
test('ErrorAlert renders the description when provided', () => {
|
||||||
const description = 'This is a detailed description';
|
const description = 'This is a detailed description';
|
||||||
render(
|
render(
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
errorType="Error"
|
errorType="Error"
|
||||||
message="Something went wrong"
|
message="Something went wrong"
|
||||||
type="error"
|
type="error"
|
||||||
description={description}
|
description={description}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(description)).toBeInTheDocument();
|
expect(screen.getByText(description)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggles description details visibility when show more/less is clicked', () => {
|
test('ErrorAlert toggles description details visibility when show more/less is clicked', () => {
|
||||||
const descriptionDetails = 'Additional details about the error.';
|
const descriptionDetails = 'Additional details about the error.';
|
||||||
render(
|
render(
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
errorType="Error"
|
errorType="Error"
|
||||||
message="Something went wrong"
|
message="Something went wrong"
|
||||||
type="error"
|
type="error"
|
||||||
descriptionDetails={descriptionDetails}
|
descriptionDetails={descriptionDetails}
|
||||||
descriptionDetailsCollapsed
|
descriptionDetailsCollapsed
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const showMoreButton = screen.getByText('See more');
|
const showMoreButton = screen.getByText('See more');
|
||||||
expect(showMoreButton).toBeInTheDocument();
|
expect(showMoreButton).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(showMoreButton);
|
fireEvent.click(showMoreButton);
|
||||||
expect(screen.getByText(descriptionDetails)).toBeInTheDocument();
|
expect(screen.getByText(descriptionDetails)).toBeInTheDocument();
|
||||||
|
|
||||||
const showLessButton = screen.getByText('See less');
|
const showLessButton = screen.getByText('See less');
|
||||||
fireEvent.click(showLessButton);
|
fireEvent.click(showLessButton);
|
||||||
expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument();
|
expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders compact mode with a tooltip and modal', () => {
|
test('ErrorAlert renders compact mode with a tooltip and modal', () => {
|
||||||
render(
|
render(
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
errorType="Error"
|
errorType="Error"
|
||||||
message="Compact mode example"
|
message="Compact mode example"
|
||||||
type="error"
|
type="error"
|
||||||
compact
|
compact
|
||||||
descriptionDetails="Detailed description in compact mode."
|
descriptionDetails="Detailed description in compact mode."
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const iconTrigger = screen.getByText('Error');
|
const iconTrigger = screen.getByText('Error');
|
||||||
expect(iconTrigger).toBeInTheDocument();
|
expect(iconTrigger).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(iconTrigger);
|
fireEvent.click(iconTrigger);
|
||||||
expect(screen.getByText('Compact mode example')).toBeInTheDocument();
|
expect(screen.getByText('Compact mode example')).toBeInTheDocument();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const missingExtraProps = {
|
|||||||
const renderComponent = (overrides = {}) =>
|
const renderComponent = (overrides = {}) =>
|
||||||
render(<InvalidSQLErrorMessage {...defaultProps} {...overrides} />);
|
render(<InvalidSQLErrorMessage {...defaultProps} {...overrides} />);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('InvalidSQLErrorMessage', () => {
|
describe('InvalidSQLErrorMessage', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.setTimeout(30000);
|
jest.setTimeout(30000);
|
||||||
@@ -64,7 +65,7 @@ describe('InvalidSQLErrorMessage', () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the error message with correct properties', async () => {
|
test('renders the error message with correct properties', async () => {
|
||||||
const { getByText, unmount } = renderComponent();
|
const { getByText, unmount } = renderComponent();
|
||||||
|
|
||||||
// Validate main properties
|
// Validate main properties
|
||||||
@@ -75,13 +76,13 @@ describe('InvalidSQLErrorMessage', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the error message with the empty extra properties', () => {
|
test('renders the error message with the empty extra properties', () => {
|
||||||
const { getByText } = renderComponent(missingExtraProps);
|
const { getByText } = renderComponent(missingExtraProps);
|
||||||
expect(getByText('Unable to parse SQL')).toBeInTheDocument();
|
expect(getByText('Unable to parse SQL')).toBeInTheDocument();
|
||||||
expect(getByText(missingExtraProps.error.message)).toBeInTheDocument();
|
expect(getByText(missingExtraProps.error.message)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the SQL error line and column indicator', async () => {
|
test('displays the SQL error line and column indicator', async () => {
|
||||||
const { getByText, container, unmount } = renderComponent();
|
const { getByText, container, unmount } = renderComponent();
|
||||||
|
|
||||||
// Validate SQL and caret indicator
|
// Validate SQL and caret indicator
|
||||||
@@ -95,7 +96,7 @@ describe('InvalidSQLErrorMessage', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles missing line number gracefully', async () => {
|
test('handles missing line number gracefully', async () => {
|
||||||
const overrides = {
|
const overrides = {
|
||||||
error: {
|
error: {
|
||||||
...defaultProps.error,
|
...defaultProps.error,
|
||||||
@@ -114,7 +115,7 @@ describe('InvalidSQLErrorMessage', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles missing column number gracefully', async () => {
|
test('handles missing column number gracefully', async () => {
|
||||||
const overrides = {
|
const overrides = {
|
||||||
error: {
|
error: {
|
||||||
...defaultProps.error,
|
...defaultProps.error,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { render, screen, fireEvent } from 'spec/helpers/testing-library';
|
|||||||
import { ErrorLevel, ErrorTypeEnum } from '@superset-ui/core';
|
import { ErrorLevel, ErrorTypeEnum } from '@superset-ui/core';
|
||||||
import { MarshmallowErrorMessage } from './MarshmallowErrorMessage';
|
import { MarshmallowErrorMessage } from './MarshmallowErrorMessage';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('MarshmallowErrorMessage', () => {
|
describe('MarshmallowErrorMessage', () => {
|
||||||
const mockError = {
|
const mockError = {
|
||||||
extra: {
|
extra: {
|
||||||
|
|||||||
@@ -99,15 +99,16 @@ const setup = (overrides = {}) => (
|
|||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('OAuth2RedirectMessage Component', () => {
|
describe('OAuth2RedirectMessage Component', () => {
|
||||||
it('renders without crashing and displays the correct initial UI elements', () => {
|
test('renders without crashing and displays the correct initial UI elements', () => {
|
||||||
const { getByText } = render(setup());
|
const { getByText } = render(setup());
|
||||||
|
|
||||||
expect(getByText(/Authorization needed/i)).toBeInTheDocument();
|
expect(getByText(/Authorization needed/i)).toBeInTheDocument();
|
||||||
expect(getByText(/provide authorization/i)).toBeInTheDocument();
|
expect(getByText(/provide authorization/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens a new window with the correct URL when the link is clicked', () => {
|
test('opens a new window with the correct URL when the link is clicked', () => {
|
||||||
const { getByText } = render(setup());
|
const { getByText } = render(setup());
|
||||||
|
|
||||||
const linkElement = getByText(/provide authorization/i);
|
const linkElement = getByText(/provide authorization/i);
|
||||||
@@ -116,7 +117,7 @@ describe('OAuth2RedirectMessage Component', () => {
|
|||||||
expect(mockOpen).toHaveBeenCalledWith('https://example.com', '_blank');
|
expect(mockOpen).toHaveBeenCalledWith('https://example.com', '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cleans up the message event listener on unmount', () => {
|
test('cleans up the message event listener on unmount', () => {
|
||||||
const { unmount } = render(setup());
|
const { unmount } = render(setup());
|
||||||
|
|
||||||
expect(mockAddEventListener).toHaveBeenCalled();
|
expect(mockAddEventListener).toHaveBeenCalled();
|
||||||
@@ -124,7 +125,7 @@ describe('OAuth2RedirectMessage Component', () => {
|
|||||||
expect(mockRemoveEventListener).toHaveBeenCalled();
|
expect(mockRemoveEventListener).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches reRunQuery action when a message with correct tab ID is received for SQL Lab', async () => {
|
test('dispatches reRunQuery action when a message with correct tab ID is received for SQL Lab', async () => {
|
||||||
render(setup());
|
render(setup());
|
||||||
|
|
||||||
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
||||||
@@ -134,7 +135,7 @@ describe('OAuth2RedirectMessage Component', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches triggerQuery action for explore source upon receiving a correct message', async () => {
|
test('dispatches triggerQuery action for explore source upon receiving a correct message', async () => {
|
||||||
render(setup({ source: 'explore' }));
|
render(setup({ source: 'explore' }));
|
||||||
|
|
||||||
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
||||||
@@ -144,7 +145,7 @@ describe('OAuth2RedirectMessage Component', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches onRefresh action for dashboard source upon receiving a correct message', async () => {
|
test('dispatches onRefresh action for dashboard source upon receiving a correct message', async () => {
|
||||||
render(setup({ source: 'dashboard' }));
|
render(setup({ source: 'dashboard' }));
|
||||||
|
|
||||||
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
|
||||||
|
|||||||
@@ -57,15 +57,16 @@ afterEach(() => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('FacePile', () => {
|
describe('FacePile', () => {
|
||||||
it('renders empty state with no users', () => {
|
test('renders empty state with no users', () => {
|
||||||
const { container } = render(<FacePile users={[]} />, { store });
|
const { container } = render(<FacePile users={[]} />, { store });
|
||||||
|
|
||||||
expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument();
|
expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument();
|
||||||
expect(container.querySelectorAll('.ant-avatar')).toHaveLength(0);
|
expect(container.querySelectorAll('.ant-avatar')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders single user without truncation', () => {
|
test('renders single user without truncation', () => {
|
||||||
const { container } = render(<FacePile users={users.slice(0, 1)} />, {
|
const { container } = render(<FacePile users={users.slice(0, 1)} />, {
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
@@ -76,7 +77,7 @@ describe('FacePile', () => {
|
|||||||
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
|
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders multiple users no truncation', () => {
|
test('renders multiple users no truncation', () => {
|
||||||
const { container } = render(<FacePile users={users.slice(0, 4)} />, {
|
const { container } = render(<FacePile users={users.slice(0, 4)} />, {
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
@@ -90,7 +91,7 @@ describe('FacePile', () => {
|
|||||||
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
|
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders multiple users with truncation', () => {
|
test('renders multiple users with truncation', () => {
|
||||||
const { container } = render(<FacePile users={users} />, { store });
|
const { container } = render(<FacePile users={users} />, { store });
|
||||||
|
|
||||||
// Should show 4 avatars + 1 overflow indicator = 5 total elements
|
// Should show 4 avatars + 1 overflow indicator = 5 total elements
|
||||||
@@ -107,7 +108,7 @@ describe('FacePile', () => {
|
|||||||
expect(within(container).getByText('+6')).toBeInTheDocument();
|
expect(within(container).getByText('+6')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays user tooltip on hover', () => {
|
test('displays user tooltip on hover', () => {
|
||||||
const { container } = render(<FacePile users={users.slice(0, 2)} />, {
|
const { container } = render(<FacePile users={users.slice(0, 2)} />, {
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
@@ -119,7 +120,7 @@ describe('FacePile', () => {
|
|||||||
expect(screen.getByRole('tooltip')).toHaveTextContent('user 0');
|
expect(screen.getByRole('tooltip')).toHaveTextContent('user 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays avatar images when Slack avatars are enabled', () => {
|
test('displays avatar images when Slack avatars are enabled', () => {
|
||||||
// Enable Slack avatars feature flag
|
// Enable Slack avatars feature flag
|
||||||
mockIsFeatureEnabled.mockImplementation(
|
mockIsFeatureEnabled.mockImplementation(
|
||||||
feature => feature === 'SLACK_ENABLE_AVATARS',
|
feature => feature === 'SLACK_ENABLE_AVATARS',
|
||||||
@@ -143,24 +144,26 @@ describe('FacePile', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('getRandomColor', () => {
|
describe('getRandomColor', () => {
|
||||||
const colors = ['color1', 'color2', 'color3'];
|
const colors = ['color1', 'color2', 'color3'];
|
||||||
|
|
||||||
it('produces the same color for the same input values', () => {
|
test('produces the same color for the same input values', () => {
|
||||||
const name = 'foo';
|
const name = 'foo';
|
||||||
expect(getRandomColor(name, colors)).toEqual(
|
expect(getRandomColor(name, colors)).toEqual(
|
||||||
getRandomColor(name, colors),
|
getRandomColor(name, colors),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('produces a different color for different input values', () => {
|
test('produces a different color for different input values', () => {
|
||||||
expect(getRandomColor('foo', colors)).not.toEqual(
|
expect(getRandomColor('foo', colors)).not.toEqual(
|
||||||
getRandomColor('bar', colors),
|
getRandomColor('bar', colors),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles non-ascii input values', () => {
|
test('handles non-ascii input values', () => {
|
||||||
expect(getRandomColor('泰', colors)).toMatchInlineSnapshot(`"color1"`);
|
expect(getRandomColor('泰', colors)).toMatchInlineSnapshot(`"color1"`);
|
||||||
expect(getRandomColor('مُحَمَّد', colors)).toMatchInlineSnapshot(
|
expect(getRandomColor('مُحَمَّد', colors)).toMatchInlineSnapshot(
|
||||||
`"color2"`,
|
`"color2"`,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||||
import { FilterableTable } from '.';
|
import { FilterableTable } from '.';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('FilterableTable', () => {
|
describe('FilterableTable', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setupAGGridModules();
|
setupAGGridModules();
|
||||||
@@ -40,10 +41,10 @@ describe('FilterableTable', () => {
|
|||||||
],
|
],
|
||||||
height: 500,
|
height: 500,
|
||||||
};
|
};
|
||||||
it('is valid element', () => {
|
test('is valid element', () => {
|
||||||
expect(isValidElement(<FilterableTable {...mockedProps} />)).toBe(true);
|
expect(isValidElement(<FilterableTable {...mockedProps} />)).toBe(true);
|
||||||
});
|
});
|
||||||
it('renders a grid with 3 Table rows', () => {
|
test('renders a grid with 3 Table rows', () => {
|
||||||
const { getByRole, getByText } = render(
|
const { getByRole, getByText } = render(
|
||||||
<FilterableTable {...mockedProps} />,
|
<FilterableTable {...mockedProps} />,
|
||||||
);
|
);
|
||||||
@@ -52,7 +53,7 @@ describe('FilterableTable', () => {
|
|||||||
expect(getByText(columnBContent)).toBeInTheDocument();
|
expect(getByText(columnBContent)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('filters on a string', () => {
|
test('filters on a string', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
filterText: 'b1',
|
filterText: 'b1',
|
||||||
@@ -62,7 +63,7 @@ describe('FilterableTable', () => {
|
|||||||
expect(queryByText('b2')).not.toBeInTheDocument();
|
expect(queryByText('b2')).not.toBeInTheDocument();
|
||||||
expect(queryByText('b3')).not.toBeInTheDocument();
|
expect(queryByText('b3')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('filters on a number', () => {
|
test('filters on a number', () => {
|
||||||
const props = {
|
const props = {
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
filterText: '100',
|
filterText: '100',
|
||||||
@@ -74,12 +75,13 @@ describe('FilterableTable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('FilterableTable sorting - RTL', () => {
|
describe('FilterableTable sorting - RTL', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setupAGGridModules();
|
setupAGGridModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts strings correctly', () => {
|
test('sorts strings correctly', () => {
|
||||||
const stringProps = {
|
const stringProps = {
|
||||||
orderedColumnKeys: ['columnA'],
|
orderedColumnKeys: ['columnA'],
|
||||||
data: [
|
data: [
|
||||||
@@ -128,7 +130,7 @@ describe('FilterableTable sorting - RTL', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts integers correctly', () => {
|
test('sorts integers correctly', () => {
|
||||||
const integerProps = {
|
const integerProps = {
|
||||||
orderedColumnKeys: ['columnB'],
|
orderedColumnKeys: ['columnB'],
|
||||||
data: [{ columnB: 21 }, { columnB: 0 }, { columnB: 623 }],
|
data: [{ columnB: 21 }, { columnB: 0 }, { columnB: 623 }],
|
||||||
@@ -163,7 +165,7 @@ describe('FilterableTable sorting - RTL', () => {
|
|||||||
expect(gridCells?.textContent).toEqual(['21', '0', '623'].join(''));
|
expect(gridCells?.textContent).toEqual(['21', '0', '623'].join(''));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts floating numbers correctly', () => {
|
test('sorts floating numbers correctly', () => {
|
||||||
const floatProps = {
|
const floatProps = {
|
||||||
orderedColumnKeys: ['columnC'],
|
orderedColumnKeys: ['columnC'],
|
||||||
data: [{ columnC: 45.67 }, { columnC: 1.23 }, { columnC: 89.0000001 }],
|
data: [{ columnC: 45.67 }, { columnC: 1.23 }, { columnC: 89.0000001 }],
|
||||||
@@ -206,7 +208,7 @@ describe('FilterableTable sorting - RTL', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts rows properly when floating numbers have mixed types', () => {
|
test('sorts rows properly when floating numbers have mixed types', () => {
|
||||||
const mixedFloatProps = {
|
const mixedFloatProps = {
|
||||||
orderedColumnKeys: ['columnD'],
|
orderedColumnKeys: ['columnD'],
|
||||||
data: [
|
data: [
|
||||||
@@ -308,7 +310,7 @@ describe('FilterableTable sorting - RTL', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sorts YYYY-MM-DD properly', () => {
|
test('sorts YYYY-MM-DD properly', () => {
|
||||||
const dsProps = {
|
const dsProps = {
|
||||||
orderedColumnKeys: ['columnDS'],
|
orderedColumnKeys: ['columnDS'],
|
||||||
data: [
|
data: [
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import { store } from 'src/views/store';
|
|
||||||
import type { FlashMessage } from './types';
|
|
||||||
import { FlashProvider } from '.';
|
|
||||||
|
|
||||||
test('Rerendering correctly with default props', () => {
|
|
||||||
const messages: FlashMessage[] = [];
|
|
||||||
render(
|
|
||||||
<FlashProvider messages={messages}>
|
|
||||||
<div data-test="my-component">My Component</div>
|
|
||||||
</FlashProvider>,
|
|
||||||
{ store },
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('my-component')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('messages should only be inserted in the State when the component is mounted', () => {
|
|
||||||
const messages: FlashMessage[] = [
|
|
||||||
['info', 'teste message 01'],
|
|
||||||
['info', 'teste message 02'],
|
|
||||||
];
|
|
||||||
expect(store.getState().messageToasts).toEqual([]);
|
|
||||||
const { rerender } = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<FlashProvider messages={messages}>
|
|
||||||
<div data-teste="my-component">My Component</div>
|
|
||||||
</FlashProvider>
|
|
||||||
</Provider>,
|
|
||||||
);
|
|
||||||
const fistRender = store.getState().messageToasts;
|
|
||||||
expect(fistRender).toHaveLength(2);
|
|
||||||
expect(fistRender[1].text).toBe(messages[0][1]);
|
|
||||||
expect(fistRender[0].text).toBe(messages[1][1]);
|
|
||||||
|
|
||||||
rerender(
|
|
||||||
<Provider store={store}>
|
|
||||||
<FlashProvider messages={[...messages, ['info', 'teste message 03']]}>
|
|
||||||
<div data-teste="my-component">My Component</div>
|
|
||||||
</FlashProvider>
|
|
||||||
</Provider>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const secondRender = store.getState().messageToasts;
|
|
||||||
expect(secondRender).toEqual(fistRender);
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
|
||||||
import { useComponentDidMount } from '@superset-ui/core';
|
|
||||||
import type { FlashMessage } from './types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: JSX.Element;
|
|
||||||
messages: FlashMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const flashObj = {
|
|
||||||
info: 'addInfoToast',
|
|
||||||
alert: 'addDangerToast',
|
|
||||||
danger: 'addDangerToast',
|
|
||||||
warning: 'addWarningToast',
|
|
||||||
success: 'addSuccessToast',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FlashProvider({ children, messages }: Props) {
|
|
||||||
const toasts = useToasts();
|
|
||||||
useComponentDidMount(() => {
|
|
||||||
messages.forEach(message => {
|
|
||||||
const [type, text] = message;
|
|
||||||
const flash = flashObj[type];
|
|
||||||
const toast = toasts[flash as keyof typeof toasts];
|
|
||||||
if (toast) {
|
|
||||||
toast(text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { FlashMessage };
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
type FlashMessageType = 'info' | 'alert' | 'danger' | 'warning' | 'success';
|
|
||||||
export type FlashMessage = [FlashMessageType, string];
|
|
||||||
@@ -152,6 +152,7 @@ test('renders unhide when invisible column exists', async () => {
|
|||||||
expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith(['column2'], true);
|
expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith(['column2'], true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('for main menu', () => {
|
describe('for main menu', () => {
|
||||||
test('renders Copy to Clipboard', async () => {
|
test('renders Copy to Clipboard', async () => {
|
||||||
const { getByText } = setup({ ...mockedProps, isMain: true });
|
const { getByText } = setup({ ...mockedProps, isMain: true });
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ const factory = (props = mockedProps) =>
|
|||||||
{ store: mockStore() },
|
{ store: mockStore() },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('ListView', () => {
|
describe('ListView', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
@@ -146,7 +147,7 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Example of converted test:
|
// Example of converted test:
|
||||||
it('calls fetchData on mount', () => {
|
test('calls fetchData on mount', () => {
|
||||||
expect(mockedProps.fetchData).toHaveBeenCalledWith({
|
expect(mockedProps.fetchData).toHaveBeenCalledWith({
|
||||||
filters: [],
|
filters: [],
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -155,7 +156,7 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchData on sort', async () => {
|
test('calls fetchData on sort', async () => {
|
||||||
const sortHeader = screen.getAllByTestId('sort-header')[1];
|
const sortHeader = screen.getAllByTestId('sort-header')[1];
|
||||||
await userEvent.click(sortHeader);
|
await userEvent.click(sortHeader);
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update pagination control tests for Ant Design pagination
|
// Update pagination control tests for Ant Design pagination
|
||||||
it('renders pagination controls', () => {
|
test('renders pagination controls', () => {
|
||||||
const paginationList = screen.getByRole('list');
|
const paginationList = screen.getByRole('list');
|
||||||
expect(paginationList).toBeInTheDocument();
|
expect(paginationList).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ describe('ListView', () => {
|
|||||||
expect(pageOneItem).toBeInTheDocument();
|
expect(pageOneItem).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchData on page change', async () => {
|
test('calls fetchData on page change', async () => {
|
||||||
const pageTwoItem = screen.getByRole('listitem', { name: '2' });
|
const pageTwoItem = screen.getByRole('listitem', { name: '2' });
|
||||||
await userEvent.click(pageTwoItem);
|
await userEvent.click(pageTwoItem);
|
||||||
|
|
||||||
@@ -197,7 +198,7 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles bulk actions on 1 row', async () => {
|
test('handles bulk actions on 1 row', async () => {
|
||||||
const checkboxes = screen.getAllByRole('checkbox');
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
await userEvent.click(checkboxes[1]); // Index 1 is the first row checkbox
|
await userEvent.click(checkboxes[1]); // Index 1 is the first row checkbox
|
||||||
|
|
||||||
@@ -217,12 +218,12 @@ describe('ListView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update UI filters test to use more specific selector
|
// Update UI filters test to use more specific selector
|
||||||
it('renders UI filters', () => {
|
test('renders UI filters', () => {
|
||||||
const filterControls = screen.getAllByRole('combobox');
|
const filterControls = screen.getAllByRole('combobox');
|
||||||
expect(filterControls).toHaveLength(2);
|
expect(filterControls).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchData on filter', async () => {
|
test('calls fetchData on filter', async () => {
|
||||||
// Handle select filter
|
// Handle select filter
|
||||||
const selectFilter = screen.getAllByRole('combobox')[0];
|
const selectFilter = screen.getAllByRole('combobox')[0];
|
||||||
await userEvent.click(selectFilter);
|
await userEvent.click(selectFilter);
|
||||||
@@ -252,7 +253,7 @@ describe('ListView', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchData on card view sort', async () => {
|
test('calls fetchData on card view sort', async () => {
|
||||||
factory({
|
factory({
|
||||||
...mockedProps,
|
...mockedProps,
|
||||||
renderCard: jest.fn(),
|
renderCard: jest.fn(),
|
||||||
|
|||||||
@@ -19,26 +19,25 @@
|
|||||||
import { ADD_TOAST, REMOVE_TOAST } from 'src/components/MessageToasts/actions';
|
import { ADD_TOAST, REMOVE_TOAST } from 'src/components/MessageToasts/actions';
|
||||||
import messageToastsReducer from 'src/components/MessageToasts/reducers';
|
import messageToastsReducer from 'src/components/MessageToasts/reducers';
|
||||||
|
|
||||||
describe('messageToasts reducer', () => {
|
// messageToasts reducer
|
||||||
it('should return initial state', () => {
|
test('messageToasts reducer should return initial state', () => {
|
||||||
expect(messageToastsReducer(undefined, {})).toEqual([]);
|
expect(messageToastsReducer(undefined, {})).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a toast', () => {
|
test('messageToasts reducer should add a toast', () => {
|
||||||
expect(
|
expect(
|
||||||
messageToastsReducer([], {
|
messageToastsReducer([], {
|
||||||
type: ADD_TOAST,
|
type: ADD_TOAST,
|
||||||
payload: { text: 'test', id: 'id', type: 'test_type' },
|
payload: { text: 'test', id: 'id', type: 'test_type' },
|
||||||
}),
|
}),
|
||||||
).toEqual([{ text: 'test', id: 'id', type: 'test_type' }]);
|
).toEqual([{ text: 'test', id: 'id', type: 'test_type' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove a toast', () => {
|
test('messageToasts reducer should remove a toast', () => {
|
||||||
expect(
|
expect(
|
||||||
messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
|
messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
|
||||||
type: REMOVE_TOAST,
|
type: REMOVE_TOAST,
|
||||||
payload: { id: 'id' },
|
payload: { id: 'id' },
|
||||||
}),
|
}),
|
||||||
).toEqual([{ id: 'id2' }]);
|
).toEqual([{ id: 'id2' }]);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { Modal } from '@superset-ui/core/components';
|
import { Modal, Loading, Flex } from '@superset-ui/core/components';
|
||||||
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
|
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
|
||||||
|
|
||||||
interface StandardModalProps {
|
interface StandardModalProps {
|
||||||
@@ -39,6 +39,7 @@ interface StandardModalProps {
|
|||||||
destroyOnClose?: boolean;
|
destroyOnClose?: boolean;
|
||||||
maskClosable?: boolean;
|
maskClosable?: boolean;
|
||||||
wrapProps?: object;
|
wrapProps?: object;
|
||||||
|
contentLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard modal widths
|
// Standard modal widths
|
||||||
@@ -113,12 +114,13 @@ export function StandardModal({
|
|||||||
destroyOnClose = true,
|
destroyOnClose = true,
|
||||||
maskClosable = false,
|
maskClosable = false,
|
||||||
wrapProps,
|
wrapProps,
|
||||||
|
contentLoading = false,
|
||||||
}: StandardModalProps) {
|
}: StandardModalProps) {
|
||||||
const primaryButtonName = saveText || (isEditMode ? t('Save') : t('Add'));
|
const primaryButtonName = saveText || (isEditMode ? t('Save') : t('Add'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledModal
|
<StyledModal
|
||||||
disablePrimaryButton={saveDisabled || saveLoading}
|
disablePrimaryButton={saveDisabled || saveLoading || contentLoading}
|
||||||
primaryButtonLoading={saveLoading}
|
primaryButtonLoading={saveLoading}
|
||||||
primaryTooltipMessage={errorTooltip}
|
primaryTooltipMessage={errorTooltip}
|
||||||
onHandledPrimaryAction={onSave}
|
onHandledPrimaryAction={onSave}
|
||||||
@@ -139,7 +141,13 @@ export function StandardModal({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{contentLoading ? (
|
||||||
|
<Flex justify="center" align="center" style={{ minHeight: 200 }}>
|
||||||
|
<Loading />
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,41 +20,40 @@ import { render, screen } from 'spec/helpers/testing-library';
|
|||||||
import { Icons } from '@superset-ui/core/components';
|
import { Icons } from '@superset-ui/core/components';
|
||||||
import { ModalTitleWithIcon } from '.';
|
import { ModalTitleWithIcon } from '.';
|
||||||
|
|
||||||
describe('ModalTitleWithIcon', () => {
|
// ModalTitleWithIcon
|
||||||
it('renders the title without icon if none is passed and isEditMode is undefined', () => {
|
test('ModalTitleWithIcon renders the title without icon if none is passed and isEditMode is undefined', () => {
|
||||||
render(<ModalTitleWithIcon title="My Title" />);
|
render(<ModalTitleWithIcon title="My Title" />);
|
||||||
expect(screen.getByText('My Title')).toBeInTheDocument();
|
expect(screen.getByText('My Title')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Edit icon if isEditMode is true', () => {
|
test('ModalTitleWithIcon renders Edit icon if isEditMode is true', () => {
|
||||||
render(<ModalTitleWithIcon title="Edit Mode" isEditMode />);
|
render(<ModalTitleWithIcon title="Edit Mode" isEditMode />);
|
||||||
expect(screen.getByText('Edit Mode')).toBeInTheDocument();
|
expect(screen.getByText('Edit Mode')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('img', { name: /edit/i })).toBeInTheDocument();
|
expect(screen.getByRole('img', { name: /edit/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Plus icon if isEditMode is false', () => {
|
test('ModalTitleWithIcon renders Plus icon if isEditMode is false', () => {
|
||||||
render(<ModalTitleWithIcon title="Add Mode" isEditMode={false} />);
|
render(<ModalTitleWithIcon title="Add Mode" isEditMode={false} />);
|
||||||
expect(screen.getByText('Add Mode')).toBeInTheDocument();
|
expect(screen.getByText('Add Mode')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('img', { name: /plus/i })).toBeInTheDocument();
|
expect(screen.getByRole('img', { name: /plus/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders custom icon when passed explicitly', () => {
|
test('ModalTitleWithIcon renders custom icon when passed explicitly', () => {
|
||||||
render(
|
render(
|
||||||
<ModalTitleWithIcon
|
<ModalTitleWithIcon
|
||||||
title="Custom Icon"
|
title="Custom Icon"
|
||||||
icon={<Icons.DownOutlined data-test="custom-icon" />}
|
icon={<Icons.DownOutlined data-test="custom-icon" />}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('Custom Icon')).toBeInTheDocument();
|
expect(screen.getByText('Custom Icon')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the level prop (e.g., renders h3 for level=3)', () => {
|
test('ModalTitleWithIcon respects the level prop (e.g., renders h3 for level=3)', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ModalTitleWithIcon title="Header Level 3" level={3} />,
|
<ModalTitleWithIcon title="Header Level 3" level={3} />,
|
||||||
);
|
);
|
||||||
expect(container.querySelector('h3')).toHaveTextContent('Header Level 3');
|
expect(container.querySelector('h3')).toHaveTextContent('Header Level 3');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import useStoredSidebarWidth from './useStoredSidebarWidth';
|
|||||||
|
|
||||||
const INITIAL_WIDTH = 300;
|
const INITIAL_WIDTH = 300;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('useStoredSidebarWidth', () => {
|
describe('useStoredSidebarWidth', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@@ -35,7 +36,7 @@ describe('useStoredSidebarWidth', () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a default filterBar width by initialWidth', () => {
|
test('returns a default filterBar width by initialWidth', () => {
|
||||||
const id = '123';
|
const id = '123';
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useStoredSidebarWidth(id, INITIAL_WIDTH),
|
useStoredSidebarWidth(id, INITIAL_WIDTH),
|
||||||
@@ -45,7 +46,7 @@ describe('useStoredSidebarWidth', () => {
|
|||||||
expect(actualWidth).toEqual(INITIAL_WIDTH);
|
expect(actualWidth).toEqual(INITIAL_WIDTH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a stored filterBar width from localStorage', () => {
|
test('returns a stored filterBar width from localStorage', () => {
|
||||||
const id = '123';
|
const id = '123';
|
||||||
const expectedWidth = 378;
|
const expectedWidth = 378;
|
||||||
setItem(LocalStorageKeys.CommonResizableSidebarWidths, {
|
setItem(LocalStorageKeys.CommonResizableSidebarWidths, {
|
||||||
@@ -61,7 +62,7 @@ describe('useStoredSidebarWidth', () => {
|
|||||||
expect(actualWidth).not.toEqual(250);
|
expect(actualWidth).not.toEqual(250);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a setter for filterBar width that stores the state in localStorage together', () => {
|
test('returns a setter for filterBar width that stores the state in localStorage together', () => {
|
||||||
const id = '123';
|
const id = '123';
|
||||||
const expectedWidth = 378;
|
const expectedWidth = 378;
|
||||||
const otherDashboardId = '456';
|
const otherDashboardId = '456';
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ const defaultProps = {
|
|||||||
datasourceType: 'table',
|
datasourceType: 'table',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('SQLEditorWithValidation', () => {
|
describe('SQLEditorWithValidation', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders SQLEditor with validation bar when showValidation is true', () => {
|
test('renders SQLEditor with validation bar when showValidation is true', () => {
|
||||||
render(<SQLEditorWithValidation {...defaultProps} />);
|
render(<SQLEditorWithValidation {...defaultProps} />);
|
||||||
|
|
||||||
expect(screen.getByText('Unverified')).toBeInTheDocument();
|
expect(screen.getByText('Unverified')).toBeInTheDocument();
|
||||||
@@ -55,7 +56,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render validation bar when showValidation is false', () => {
|
test('does not render validation bar when showValidation is false', () => {
|
||||||
render(
|
render(
|
||||||
<SQLEditorWithValidation {...defaultProps} showValidation={false} />,
|
<SQLEditorWithValidation {...defaultProps} showValidation={false} />,
|
||||||
);
|
);
|
||||||
@@ -66,7 +67,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows primary button style when unverified', () => {
|
test('shows primary button style when unverified', () => {
|
||||||
render(<SQLEditorWithValidation {...defaultProps} />);
|
render(<SQLEditorWithValidation {...defaultProps} />);
|
||||||
|
|
||||||
const validateButton = screen.getByRole('button', {
|
const validateButton = screen.getByRole('button', {
|
||||||
@@ -76,7 +77,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
// Button should have primary styling (this would need to check actual class or style)
|
// Button should have primary styling (this would need to check actual class or style)
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables validate button when no value or datasourceId', () => {
|
test('disables validate button when no value or datasourceId', () => {
|
||||||
render(
|
render(
|
||||||
<SQLEditorWithValidation
|
<SQLEditorWithValidation
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
@@ -91,7 +92,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
expect(validateButton).toBeDisabled();
|
expect(validateButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows validating state when validation is in progress', async () => {
|
test('shows validating state when validation is in progress', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -117,7 +118,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows success state when validation passes', async () => {
|
test('shows success state when validation passes', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -138,7 +139,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
expect(validateButton).toBeInTheDocument();
|
expect(validateButton).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows error state when validation fails', async () => {
|
test('shows error state when validation fails', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -169,7 +170,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles API errors gracefully', async () => {
|
test('handles API errors gracefully', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -189,7 +190,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends correct payload for column expression', async () => {
|
test('sends correct payload for column expression', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -221,7 +222,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends correct payload for WHERE expression', async () => {
|
test('sends correct payload for WHERE expression', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -252,7 +253,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends correct payload for HAVING expression', async () => {
|
test('sends correct payload for HAVING expression', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
@@ -283,7 +284,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets validation state when value changes', () => {
|
test('resets validation state when value changes', () => {
|
||||||
const { rerender } = render(<SQLEditorWithValidation {...defaultProps} />);
|
const { rerender } = render(<SQLEditorWithValidation {...defaultProps} />);
|
||||||
|
|
||||||
// Simulate having a validation result
|
// Simulate having a validation result
|
||||||
@@ -304,7 +305,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
expect(screen.getByText('Unverified')).toBeInTheDocument();
|
expect(screen.getByText('Unverified')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onChange when editor value changes', () => {
|
test('calls onChange when editor value changes', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
render(<SQLEditorWithValidation {...defaultProps} onChange={onChange} />);
|
render(<SQLEditorWithValidation {...defaultProps} onChange={onChange} />);
|
||||||
|
|
||||||
@@ -313,7 +314,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
expect(onChange).toBeDefined();
|
expect(onChange).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onValidationComplete callback when provided', async () => {
|
test('calls onValidationComplete callback when provided', async () => {
|
||||||
const onValidationComplete = jest.fn();
|
const onValidationComplete = jest.fn();
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
@@ -337,7 +338,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onValidationComplete with errors when validation fails', async () => {
|
test('calls onValidationComplete with errors when validation fails', async () => {
|
||||||
const onValidationComplete = jest.fn();
|
const onValidationComplete = jest.fn();
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
@@ -371,7 +372,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows tooltip with full error message when error is truncated', async () => {
|
test('shows tooltip with full error message when error is truncated', async () => {
|
||||||
const longErrorMessage =
|
const longErrorMessage =
|
||||||
'This is a very long error message that should be truncated in the display but shown in full in the tooltip when user hovers over it';
|
'This is a very long error message that should be truncated in the display but shown in full in the tooltip when user hovers over it';
|
||||||
|
|
||||||
@@ -410,7 +411,7 @@ describe('SQLEditorWithValidation', () => {
|
|||||||
expect(errorElement.parentElement).toBeTruthy();
|
expect(errorElement.parentElement).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty response gracefully', async () => {
|
test('handles empty response gracefully', async () => {
|
||||||
const mockPost = SupersetClient.post as jest.MockedFunction<
|
const mockPost = SupersetClient.post as jest.MockedFunction<
|
||||||
typeof SupersetClient.post
|
typeof SupersetClient.post
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import fetchMock from 'fetch-mock';
|
|||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
import { tagToSelectOption, loadTags } from 'src/components/Tag/utils';
|
import { tagToSelectOption, loadTags } from 'src/components/Tag/utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('tagToSelectOption', () => {
|
describe('tagToSelectOption', () => {
|
||||||
test('converts a Tag object with table_name to a SelectTagsValue', () => {
|
test('converts a Tag object with table_name to a SelectTagsValue', () => {
|
||||||
const tag = {
|
const tag = {
|
||||||
@@ -38,6 +39,7 @@ describe('tagToSelectOption', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('loadTags', () => {
|
describe('loadTags', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ test('should render 3 elements when maxTags is set to 3', async () => {
|
|||||||
expect(tagsListItems[2]).toHaveTextContent('+3...');
|
expect(tagsListItems[2]).toHaveTextContent('+3...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Tag type filtering', () => {
|
describe('Tag type filtering', () => {
|
||||||
test('should render only custom type tags (type: 1)', async () => {
|
test('should render only custom type tags (type: 1)', async () => {
|
||||||
const mixedTypeTags = [
|
const mixedTypeTags = [
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export * from './ErrorMessage';
|
|||||||
export { ImportModal, type ImportModelsModalProps } from './ImportModal';
|
export { ImportModal, type ImportModelsModalProps } from './ImportModal';
|
||||||
export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary';
|
export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary';
|
||||||
export * from './GenericLink';
|
export * from './GenericLink';
|
||||||
export { FlashProvider, type FlashMessage } from './FlashProvider';
|
|
||||||
export { GridTable, type TableProps } from './GridTable';
|
export { GridTable, type TableProps } from './GridTable';
|
||||||
export * from './Tag';
|
export * from './Tag';
|
||||||
export * from './TagsList';
|
export * from './TagsList';
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ export const RESERVED_DASHBOARD_URL_PARAMS: string[] = [
|
|||||||
export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
|
export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
|
||||||
application_root: '/',
|
application_root: '/',
|
||||||
static_assets_prefix: '',
|
static_assets_prefix: '',
|
||||||
flash_messages: [],
|
|
||||||
conf: {},
|
conf: {},
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
feature_flags: {},
|
feature_flags: {},
|
||||||
@@ -171,6 +170,7 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
|
|||||||
},
|
},
|
||||||
d3_format: DEFAULT_D3_FORMAT,
|
d3_format: DEFAULT_D3_FORMAT,
|
||||||
d3_time_format: DEFAULT_D3_TIME_FORMAT,
|
d3_time_format: DEFAULT_D3_TIME_FORMAT,
|
||||||
|
pdf_compression_level: 'MEDIUM',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = {
|
export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
NEW_ROW_ID,
|
NEW_ROW_ID,
|
||||||
} from 'src/dashboard/util/constants';
|
} from 'src/dashboard/util/constants';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('dashboardLayout actions', () => {
|
describe('dashboardLayout actions', () => {
|
||||||
const mockState = {
|
const mockState = {
|
||||||
dashboardState: {
|
dashboardState: {
|
||||||
@@ -85,8 +86,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
dashboardFilters.updateLayoutComponents.restore();
|
dashboardFilters.updateLayoutComponents.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('updateComponents', () => {
|
describe('updateComponents', () => {
|
||||||
it('should dispatch an updateLayout action', () => {
|
test('should dispatch an updateLayout action', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const nextComponents = { 1: {} };
|
const nextComponents = { 1: {} };
|
||||||
const thunk = updateComponents(nextComponents);
|
const thunk = updateComponents(nextComponents);
|
||||||
@@ -101,7 +103,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -115,8 +117,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('deleteComponents', () => {
|
describe('deleteComponents', () => {
|
||||||
it('should dispatch an deleteComponent action', () => {
|
test('should dispatch an deleteComponent action', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const thunk = deleteComponent('id', 'parentId');
|
const thunk = deleteComponent('id', 'parentId');
|
||||||
thunk(dispatch, getState);
|
thunk(dispatch, getState);
|
||||||
@@ -129,7 +132,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -142,8 +145,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('updateDashboardTitle', () => {
|
describe('updateDashboardTitle', () => {
|
||||||
it('should dispatch an updateComponent action for the header component', () => {
|
test('should dispatch an updateComponent action for the header component', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const thunk1 = updateDashboardTitle('new text');
|
const thunk1 = updateDashboardTitle('new text');
|
||||||
thunk1(dispatch, getState);
|
thunk1(dispatch, getState);
|
||||||
@@ -167,8 +171,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('createTopLevelTabs', () => {
|
describe('createTopLevelTabs', () => {
|
||||||
it('should dispatch a createTopLevelTabs action', () => {
|
test('should dispatch a createTopLevelTabs action', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const dropResult = {};
|
const dropResult = {};
|
||||||
const thunk = createTopLevelTabs(dropResult);
|
const thunk = createTopLevelTabs(dropResult);
|
||||||
@@ -182,7 +187,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -196,8 +201,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('deleteTopLevelTabs', () => {
|
describe('deleteTopLevelTabs', () => {
|
||||||
it('should dispatch a deleteTopLevelTabs action', () => {
|
test('should dispatch a deleteTopLevelTabs action', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const dropResult = {};
|
const dropResult = {};
|
||||||
const thunk = deleteTopLevelTabs(dropResult);
|
const thunk = deleteTopLevelTabs(dropResult);
|
||||||
@@ -211,7 +217,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -225,6 +231,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('resizeComponent', () => {
|
describe('resizeComponent', () => {
|
||||||
const dashboardLayout = {
|
const dashboardLayout = {
|
||||||
...mockState.dashboardLayout,
|
...mockState.dashboardLayout,
|
||||||
@@ -240,7 +247,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should update the size of the component', () => {
|
test('should update the size of the component', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
});
|
});
|
||||||
@@ -271,7 +278,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatch.callCount).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
dashboardLayout,
|
dashboardLayout,
|
||||||
@@ -289,8 +296,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('handleComponentDrop', () => {
|
describe('handleComponentDrop', () => {
|
||||||
it('should create a component if it is new', () => {
|
test('should create a component if it is new', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const dropResult = {
|
const dropResult = {
|
||||||
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
||||||
@@ -315,7 +323,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move a component if the component is not new', () => {
|
test('should move a component if the component is not new', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: {
|
dashboardLayout: {
|
||||||
// if 'dragging' is not only child will dispatch deleteComponent thunk
|
// if 'dragging' is not only child will dispatch deleteComponent thunk
|
||||||
@@ -345,7 +353,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a toast if the drop overflows the destination', () => {
|
test('should dispatch a toast if the drop overflows the destination', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: {
|
dashboardLayout: {
|
||||||
present: {
|
present: {
|
||||||
@@ -374,7 +382,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dispatch.callCount).toBe(1);
|
expect(dispatch.callCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a parent Row or Tabs if the moved child was the only child', () => {
|
test('should delete a parent Row or Tabs if the moved child was the only child', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: {
|
dashboardLayout: {
|
||||||
present: {
|
present: {
|
||||||
@@ -411,7 +419,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create top-level tabs if dropped on root', () => {
|
test('should create top-level tabs if dropped on root', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const dropResult = {
|
const dropResult = {
|
||||||
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
source: { id: NEW_COMPONENTS_SOURCE_ID },
|
||||||
@@ -433,7 +441,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a toast if drop top-level tab into nested tab', () => {
|
test('should dispatch a toast if drop top-level tab into nested tab', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: {
|
dashboardLayout: {
|
||||||
present: {
|
present: {
|
||||||
@@ -488,8 +496,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('undoLayoutAction', () => {
|
describe('undoLayoutAction', () => {
|
||||||
it('should dispatch a redux-undo .undo() action', () => {
|
test('should dispatch a redux-undo .undo() action', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: { past: ['non-empty'] },
|
dashboardLayout: { past: ['non-empty'] },
|
||||||
});
|
});
|
||||||
@@ -500,7 +509,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.undo());
|
expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.undo());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges(false) action history length is zero', () => {
|
test('should dispatch a setUnsavedChanges(false) action history length is zero', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardLayout: { past: [] },
|
dashboardLayout: { past: [] },
|
||||||
});
|
});
|
||||||
@@ -512,8 +521,9 @@ describe('dashboardLayout actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('redoLayoutAction', () => {
|
describe('redoLayoutAction', () => {
|
||||||
it('should dispatch a redux-undo .redo() action', () => {
|
test('should dispatch a redux-undo .redo() action', () => {
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const thunk = redoLayoutAction();
|
const thunk = redoLayoutAction();
|
||||||
thunk(dispatch, getState);
|
thunk(dispatch, getState);
|
||||||
@@ -524,7 +534,7 @@ describe('dashboardLayout actions', () => {
|
|||||||
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => {
|
test('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ jest.mock('@superset-ui/core', () => ({
|
|||||||
isFeatureEnabled: jest.fn(),
|
isFeatureEnabled: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('dashboardState actions', () => {
|
describe('dashboardState actions', () => {
|
||||||
const mockState = {
|
const mockState = {
|
||||||
dashboardState: {
|
dashboardState: {
|
||||||
@@ -101,8 +102,9 @@ describe('dashboardState actions', () => {
|
|||||||
return { getState, dispatch, state };
|
return { getState, dispatch, state };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('saveDashboardRequest', () => {
|
describe('saveDashboardRequest', () => {
|
||||||
it('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
|
test('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -115,7 +117,7 @@ describe('dashboardState actions', () => {
|
|||||||
expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED);
|
expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should post dashboard data with updated redux state', () => {
|
test('should post dashboard data with updated redux state', () => {
|
||||||
const { getState, dispatch } = setup({
|
const { getState, dispatch } = setup({
|
||||||
dashboardState: { hasUnsavedChanges: false },
|
dashboardState: { hasUnsavedChanges: false },
|
||||||
});
|
});
|
||||||
@@ -144,6 +146,7 @@ describe('dashboardState actions', () => {
|
|||||||
).toStrictEqual(mockParentsList);
|
).toStrictEqual(mockParentsList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => {
|
describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
isFeatureEnabled.mockImplementation(
|
isFeatureEnabled.mockImplementation(
|
||||||
@@ -155,7 +158,7 @@ describe('dashboardState actions', () => {
|
|||||||
isFeatureEnabled.mockRestore();
|
isFeatureEnabled.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches SET_OVERRIDE_CONFIRM when an inspect value has diff', async () => {
|
test('dispatches SET_OVERRIDE_CONFIRM when an inspect value has diff', async () => {
|
||||||
const id = 192;
|
const id = 192;
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const thunk = saveDashboardRequest(
|
const thunk = saveDashboardRequest(
|
||||||
@@ -174,7 +177,7 @@ describe('dashboardState actions', () => {
|
|||||||
).toBe(id);
|
).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should post dashboard data with after confirm the overwrite values', async () => {
|
test('should post dashboard data with after confirm the overwrite values', async () => {
|
||||||
const id = 192;
|
const id = 192;
|
||||||
const { getState, dispatch } = setup();
|
const { getState, dispatch } = setup();
|
||||||
const confirmedDashboardData = {
|
const confirmedDashboardData = {
|
||||||
|
|||||||
@@ -274,7 +274,6 @@ export const hydrateDashboard =
|
|||||||
superset_can_csv: findPermission('can_csv', 'Superset', roles),
|
superset_can_csv: findPermission('can_csv', 'Superset', roles),
|
||||||
common: {
|
common: {
|
||||||
// legacy, please use state.common instead
|
// legacy, please use state.common instead
|
||||||
flash_messages: common?.flash_messages,
|
|
||||||
conf: common?.conf,
|
conf: common?.conf,
|
||||||
},
|
},
|
||||||
filterBarOrientation:
|
filterBarOrientation:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { render, act } from 'spec/helpers/testing-library';
|
import { render, act } from 'spec/helpers/testing-library';
|
||||||
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
import AnchorLink from 'src/dashboard/components/AnchorLink';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('AnchorLink', () => {
|
describe('AnchorLink', () => {
|
||||||
const props = {
|
const props = {
|
||||||
id: 'CHART-123',
|
id: 'CHART-123',
|
||||||
@@ -30,7 +31,7 @@ describe('AnchorLink', () => {
|
|||||||
window.location = globalLocation;
|
window.location = globalLocation;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should scroll the AnchorLink into view upon mount if id matches hash', async () => {
|
test('should scroll the AnchorLink into view upon mount if id matches hash', async () => {
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
jest.spyOn(document, 'getElementById').mockReturnValue({
|
jest.spyOn(document, 'getElementById').mockReturnValue({
|
||||||
scrollIntoView: callback,
|
scrollIntoView: callback,
|
||||||
@@ -49,7 +50,7 @@ describe('AnchorLink', () => {
|
|||||||
expect(callback).toHaveBeenCalledTimes(1);
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render anchor link without short link button', () => {
|
test('should render anchor link without short link button', () => {
|
||||||
const { container, queryByRole } = render(
|
const { container, queryByRole } = render(
|
||||||
<AnchorLink showShortLinkButton={false} {...props} />,
|
<AnchorLink showShortLinkButton={false} {...props} />,
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
@@ -58,7 +59,7 @@ describe('AnchorLink', () => {
|
|||||||
expect(queryByRole('button')).not.toBeInTheDocument();
|
expect(queryByRole('button')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render short link button', () => {
|
test('should render short link button', () => {
|
||||||
const { getByRole } = render(
|
const { getByRole } = render(
|
||||||
<AnchorLink {...props} showShortLinkButton />,
|
<AnchorLink {...props} showShortLinkButton />,
|
||||||
{ useRedux: true },
|
{ useRedux: true },
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import Tabs from '@superset-ui/core/components/Tabs';
|
import Tabs from '@superset-ui/core/components/Tabs';
|
||||||
import { t, css, SupersetTheme, useTheme } from '@superset-ui/core';
|
import { t, css, SupersetTheme } from '@superset-ui/core';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import SliceAdder from 'src/dashboard/containers/SliceAdder';
|
import SliceAdder from 'src/dashboard/containers/SliceAdder';
|
||||||
import dashboardComponents from 'src/visualizations/presets/dashboardComponents';
|
import dashboardComponents from 'src/visualizations/presets/dashboardComponents';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import NewColumn from '../gridComponents/new/NewColumn';
|
import NewColumn from '../gridComponents/new/NewColumn';
|
||||||
import NewDivider from '../gridComponents/new/NewDivider';
|
import NewDivider from '../gridComponents/new/NewDivider';
|
||||||
import NewHeader from '../gridComponents/new/NewHeader';
|
import NewHeader from '../gridComponents/new/NewHeader';
|
||||||
@@ -39,98 +37,83 @@ const TABS_KEYS = {
|
|||||||
LAYOUT_ELEMENTS: 'LAYOUT_ELEMENTS',
|
LAYOUT_ELEMENTS: 'LAYOUT_ELEMENTS',
|
||||||
};
|
};
|
||||||
|
|
||||||
const BuilderComponentPane = ({ topOffset = 0 }) => {
|
const BuilderComponentPane = ({ topOffset = 0 }) => (
|
||||||
const theme = useTheme();
|
<div
|
||||||
const nativeFiltersBarOpen = useSelector(
|
data-test="dashboard-builder-sidepane"
|
||||||
(state: any) => state.dashboardState.nativeFiltersBarOpen ?? false,
|
css={css`
|
||||||
);
|
position: sticky;
|
||||||
|
right: 0;
|
||||||
const tabBarStyle = useMemo(
|
top: ${topOffset}px;
|
||||||
() => ({
|
height: calc(100vh - ${topOffset}px);
|
||||||
paddingLeft: nativeFiltersBarOpen ? 0 : theme.sizeUnit * 4,
|
width: ${BUILDER_PANE_WIDTH}px;
|
||||||
}),
|
`}
|
||||||
[nativeFiltersBarOpen, theme.sizeUnit],
|
>
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
data-test="dashboard-builder-sidepane"
|
css={(theme: SupersetTheme) => css`
|
||||||
css={css`
|
position: absolute;
|
||||||
position: sticky;
|
height: 100%;
|
||||||
right: 0;
|
|
||||||
top: ${topOffset}px;
|
|
||||||
height: calc(100vh - ${topOffset}px);
|
|
||||||
width: ${BUILDER_PANE_WIDTH}px;
|
width: ${BUILDER_PANE_WIDTH}px;
|
||||||
|
box-shadow: -${theme.sizeUnit}px 0 ${theme.sizeUnit}px 0
|
||||||
|
${tinycolor(theme.colorBorder).setAlpha(0.1).toRgbString()};
|
||||||
|
background-color: ${theme.colorBgBase};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div
|
<Tabs
|
||||||
|
data-test="dashboard-builder-component-pane-tabs-navigation"
|
||||||
|
id="tabs"
|
||||||
css={(theme: SupersetTheme) => css`
|
css={(theme: SupersetTheme) => css`
|
||||||
position: absolute;
|
line-height: inherit;
|
||||||
|
margin-top: ${theme.sizeUnit * 2}px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: ${BUILDER_PANE_WIDTH}px;
|
|
||||||
box-shadow: -4px 0 4px 0
|
|
||||||
${tinycolor(theme.colorBorder).setAlpha(0.1).toRgbString()};
|
|
||||||
background-color: ${theme.colorBgBase};
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
data-test="dashboard-builder-component-pane-tabs-navigation"
|
|
||||||
id="tabs"
|
|
||||||
tabBarStyle={tabBarStyle}
|
|
||||||
css={(theme: SupersetTheme) => css`
|
|
||||||
line-height: inherit;
|
|
||||||
margin-top: ${theme.sizeUnit * 2}px;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
& .ant-tabs-content-holder {
|
& .ant-tabs-content-holder {
|
||||||
|
height: 100%;
|
||||||
|
& .ant-tabs-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
& .ant-tabs-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`}
|
}
|
||||||
items={[
|
`}
|
||||||
{
|
items={[
|
||||||
key: TABS_KEYS.CHARTS,
|
{
|
||||||
label: t('Charts'),
|
key: TABS_KEYS.CHARTS,
|
||||||
children: (
|
label: t('Charts'),
|
||||||
<div
|
children: (
|
||||||
css={css`
|
<div
|
||||||
height: calc(100vh - ${topOffset * 2}px);
|
css={css`
|
||||||
`}
|
height: calc(100vh - ${topOffset * 2}px);
|
||||||
>
|
`}
|
||||||
<SliceAdder />
|
>
|
||||||
</div>
|
<SliceAdder />
|
||||||
),
|
</div>
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
key: TABS_KEYS.LAYOUT_ELEMENTS,
|
{
|
||||||
label: t('Layout elements'),
|
key: TABS_KEYS.LAYOUT_ELEMENTS,
|
||||||
children: (
|
label: t('Layout elements'),
|
||||||
<>
|
children: (
|
||||||
<NewTabs />
|
<>
|
||||||
<NewRow />
|
<NewTabs />
|
||||||
<NewColumn />
|
<NewRow />
|
||||||
<NewHeader />
|
<NewColumn />
|
||||||
<NewMarkdown />
|
<NewHeader />
|
||||||
<NewDivider />
|
<NewMarkdown />
|
||||||
{dashboardComponents
|
<NewDivider />
|
||||||
.getAll()
|
{dashboardComponents
|
||||||
.map(({ key: componentKey, metadata }) => (
|
.getAll()
|
||||||
<NewDynamicComponent
|
.map(({ key: componentKey, metadata }) => (
|
||||||
key={componentKey}
|
<NewDynamicComponent
|
||||||
metadata={metadata}
|
key={componentKey}
|
||||||
componentKey={componentKey}
|
metadata={metadata}
|
||||||
/>
|
componentKey={componentKey}
|
||||||
))}
|
/>
|
||||||
</>
|
))}
|
||||||
),
|
</>
|
||||||
},
|
),
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default BuilderComponentPane;
|
export default BuilderComponentPane;
|
||||||
|
|||||||
@@ -120,15 +120,12 @@ class Dashboard extends PureComponent {
|
|||||||
this.applyCharts();
|
this.applyCharts();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps) {
|
||||||
this.applyCharts();
|
this.applyCharts();
|
||||||
}
|
const currentChartIds = getChartIdsFromLayout(prevProps.layout);
|
||||||
|
const nextChartIds = getChartIdsFromLayout(this.props.layout);
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
if (prevProps.dashboardId !== this.props.dashboardId) {
|
||||||
const currentChartIds = getChartIdsFromLayout(this.props.layout);
|
|
||||||
const nextChartIds = getChartIdsFromLayout(nextProps.layout);
|
|
||||||
|
|
||||||
if (this.props.dashboardId !== nextProps.dashboardId) {
|
|
||||||
// single-page-app navigation check
|
// single-page-app navigation check
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -140,7 +137,7 @@ class Dashboard extends PureComponent {
|
|||||||
newChartIds.forEach(newChartId =>
|
newChartIds.forEach(newChartId =>
|
||||||
this.props.actions.addSliceToDashboard(
|
this.props.actions.addSliceToDashboard(
|
||||||
newChartId,
|
newChartId,
|
||||||
getLayoutComponentFromChartId(nextProps.layout, newChartId),
|
getLayoutComponentFromChartId(this.props.layout, newChartId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if (currentChartIds.length > nextChartIds.length) {
|
} else if (currentChartIds.length > nextChartIds.length) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { getRelatedCharts } from 'src/dashboard/util/getRelatedCharts';
|
|||||||
|
|
||||||
jest.mock('src/dashboard/util/getRelatedCharts');
|
jest.mock('src/dashboard/util/getRelatedCharts');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('Dashboard', () => {
|
describe('Dashboard', () => {
|
||||||
const mockAddSlice = jest.fn();
|
const mockAddSlice = jest.fn();
|
||||||
const mockRemoveSlice = jest.fn();
|
const mockRemoveSlice = jest.fn();
|
||||||
@@ -91,18 +92,19 @@ describe('Dashboard', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the children component', () => {
|
test('should render the children component', () => {
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('layout changes', () => {
|
describe('layout changes', () => {
|
||||||
const layoutWithExtraChart = {
|
const layoutWithExtraChart = {
|
||||||
...props.layout,
|
...props.layout,
|
||||||
1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }),
|
1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should call addSliceToDashboard if a new slice is added to the layout', () => {
|
test('should call addSliceToDashboard if a new slice is added to the layout', () => {
|
||||||
const { rerender } = renderDashboard();
|
const { rerender } = renderDashboard();
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
@@ -116,7 +118,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockAddSlice).toHaveBeenCalled();
|
expect(mockAddSlice).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call removeSliceFromDashboard if a slice is removed from the layout', () => {
|
test('should call removeSliceFromDashboard if a slice is removed from the layout', () => {
|
||||||
const { rerender } = renderDashboard({ layout: layoutWithExtraChart });
|
const { rerender } = renderDashboard({ layout: layoutWithExtraChart });
|
||||||
|
|
||||||
const nextLayout = { ...layoutWithExtraChart };
|
const nextLayout = { ...layoutWithExtraChart };
|
||||||
@@ -134,8 +136,9 @@ describe('Dashboard', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('filter updates', () => {
|
describe('filter updates', () => {
|
||||||
it('should not call refresh when in editMode', () => {
|
test('should not call refresh when in editMode', () => {
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
@@ -156,7 +159,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).not.toHaveBeenCalled();
|
expect(mockTriggerQuery).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call refresh when there is no change', () => {
|
test('should not call refresh when there is no change', () => {
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
@@ -170,7 +173,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).not.toHaveBeenCalled();
|
expect(mockTriggerQuery).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh when native filters changed', () => {
|
test('should call refresh when native filters changed', () => {
|
||||||
getRelatedCharts.mockReturnValue([230]);
|
getRelatedCharts.mockReturnValue([230]);
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -195,7 +198,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalled();
|
expect(mockTriggerQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh if a filter is added', () => {
|
test('should call refresh if a filter is added', () => {
|
||||||
getRelatedCharts.mockReturnValue([1]);
|
getRelatedCharts.mockReturnValue([1]);
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalled();
|
expect(mockTriggerQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh if a filter is removed', () => {
|
test('should call refresh if a filter is removed', () => {
|
||||||
getRelatedCharts.mockReturnValue([1]); // Ensure we return some charts to refresh
|
getRelatedCharts.mockReturnValue([1]); // Ensure we return some charts to refresh
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -233,7 +236,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalledWith(true, 1);
|
expect(mockTriggerQuery).toHaveBeenCalledWith(true, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh if a filter is changed', () => {
|
test('should call refresh if a filter is changed', () => {
|
||||||
getRelatedCharts.mockReturnValue([1]);
|
getRelatedCharts.mockReturnValue([1]);
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -253,7 +256,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalled();
|
expect(mockTriggerQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh with multiple chart ids', () => {
|
test('should call refresh with multiple chart ids', () => {
|
||||||
getRelatedCharts.mockReturnValue([1, 2]);
|
getRelatedCharts.mockReturnValue([1, 2]);
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -273,7 +276,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalled();
|
expect(mockTriggerQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh if a filter scope is changed', () => {
|
test('should call refresh if a filter scope is changed', () => {
|
||||||
getRelatedCharts.mockReturnValue([2]);
|
getRelatedCharts.mockReturnValue([2]);
|
||||||
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
|
||||||
|
|
||||||
@@ -293,7 +296,7 @@ describe('Dashboard', () => {
|
|||||||
expect(mockTriggerQuery).toHaveBeenCalled();
|
expect(mockTriggerQuery).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh with empty [] if a filter is changed but scope is not applicable', () => {
|
test('should call refresh with empty [] if a filter is changed but scope is not applicable', () => {
|
||||||
getRelatedCharts.mockReturnValue([]);
|
getRelatedCharts.mockReturnValue([]);
|
||||||
const { rerender } = renderDashboard({
|
const { rerender } = renderDashboard({
|
||||||
activeFilters: OVERRIDE_FILTERS,
|
activeFilters: OVERRIDE_FILTERS,
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ jest.mock('src/dashboard/containers/DashboardGrid', () => () => (
|
|||||||
<div data-test="mock-dashboard-grid" />
|
<div data-test="mock-dashboard-grid" />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('DashboardBuilder', () => {
|
describe('DashboardBuilder', () => {
|
||||||
let favStarStub: jest.Mock;
|
let favStarStub: jest.Mock;
|
||||||
let activeTabsStub: jest.Mock;
|
let activeTabsStub: jest.Mock;
|
||||||
@@ -120,13 +121,13 @@ describe('DashboardBuilder', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should render a StickyContainer with class "dashboard"', () => {
|
test('should render a StickyContainer with class "dashboard"', () => {
|
||||||
const { getByTestId } = setup();
|
const { getByTestId } = setup();
|
||||||
const stickyContainer = getByTestId('dashboard-content-wrapper');
|
const stickyContainer = getByTestId('dashboard-content-wrapper');
|
||||||
expect(stickyContainer).toHaveClass('dashboard');
|
expect(stickyContainer).toHaveClass('dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add the "dashboard--editing" class if editMode=true', () => {
|
test('should add the "dashboard--editing" class if editMode=true', () => {
|
||||||
const { getByTestId } = setup({
|
const { getByTestId } = setup({
|
||||||
dashboardState: { ...mockState.dashboardState, editMode: true },
|
dashboardState: { ...mockState.dashboardState, editMode: true },
|
||||||
});
|
});
|
||||||
@@ -134,13 +135,13 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(stickyContainer).toHaveClass('dashboard dashboard--editing');
|
expect(stickyContainer).toHaveClass('dashboard dashboard--editing');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a DragDroppable DashboardHeader', () => {
|
test('should render a DragDroppable DashboardHeader', () => {
|
||||||
const { queryByTestId } = setup();
|
const { queryByTestId } = setup();
|
||||||
const header = queryByTestId('dashboard-header-container');
|
const header = queryByTestId('dashboard-header-container');
|
||||||
expect(header).toBeInTheDocument();
|
expect(header).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a Sticky top-level Tabs if the dashboard has tabs', async () => {
|
test('should render a Sticky top-level Tabs if the dashboard has tabs', async () => {
|
||||||
const { findAllByTestId } = setup({
|
const { findAllByTestId } = setup({
|
||||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||||
});
|
});
|
||||||
@@ -162,7 +163,7 @@ describe('DashboardBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render one Tabs and two TabPane', async () => {
|
test('should render one Tabs and two TabPane', async () => {
|
||||||
const { findAllByRole } = setup({
|
const { findAllByRole } = setup({
|
||||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||||
});
|
});
|
||||||
@@ -172,7 +173,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(tabPanels.length).toBe(2);
|
expect(tabPanels.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a TabPane and DashboardGrid for first Tab', async () => {
|
test('should render a TabPane and DashboardGrid for first Tab', async () => {
|
||||||
const { findByTestId } = setup({
|
const { findByTestId } = setup({
|
||||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||||
});
|
});
|
||||||
@@ -188,7 +189,7 @@ describe('DashboardBuilder', () => {
|
|||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a TabPane and DashboardGrid for second Tab', async () => {
|
test('should render a TabPane and DashboardGrid for second Tab', async () => {
|
||||||
const { findByTestId } = setup({
|
const { findByTestId } = setup({
|
||||||
dashboardLayout: undoableDashboardLayoutWithTabs,
|
dashboardLayout: undoableDashboardLayoutWithTabs,
|
||||||
dashboardState: {
|
dashboardState: {
|
||||||
@@ -209,13 +210,13 @@ describe('DashboardBuilder', () => {
|
|||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
|
test('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
|
||||||
const { queryAllByTestId } = setup();
|
const { queryAllByTestId } = setup();
|
||||||
const builderComponents = queryAllByTestId('mock-builder-component-pane');
|
const builderComponents = queryAllByTestId('mock-builder-component-pane');
|
||||||
expect(builderComponents.length).toBe(0);
|
expect(builderComponents.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => {
|
test('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => {
|
||||||
const { queryAllByTestId } = setup({
|
const { queryAllByTestId } = setup({
|
||||||
dashboardState: { ...mockState.dashboardState, editMode: true },
|
dashboardState: { ...mockState.dashboardState, editMode: true },
|
||||||
});
|
});
|
||||||
@@ -223,7 +224,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(builderComponents.length).toBeGreaterThanOrEqual(1);
|
expect(builderComponents.length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change redux state if a top-level Tab is clicked', async () => {
|
test('should change redux state if a top-level Tab is clicked', async () => {
|
||||||
(setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({
|
(setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({
|
||||||
type: 'type',
|
type: 'type',
|
||||||
arg0,
|
arg0,
|
||||||
@@ -243,13 +244,13 @@ describe('DashboardBuilder', () => {
|
|||||||
(setDirectPathToChild as jest.Mock).mockReset();
|
(setDirectPathToChild as jest.Mock).mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not display a loading spinner when saving is not in progress', () => {
|
test('should not display a loading spinner when saving is not in progress', () => {
|
||||||
const { queryByTestId } = setup();
|
const { queryByTestId } = setup();
|
||||||
|
|
||||||
expect(queryByTestId('loading-indicator')).not.toBeInTheDocument();
|
expect(queryByTestId('loading-indicator')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display a loading spinner when saving is in progress', async () => {
|
test('should display a loading spinner when saving is in progress', async () => {
|
||||||
const { findByTestId } = setup({
|
const { findByTestId } = setup({
|
||||||
dashboardState: { ...mockState.dashboardState, dashboardIsSaving: true },
|
dashboardState: { ...mockState.dashboardState, dashboardIsSaving: true },
|
||||||
});
|
});
|
||||||
@@ -257,7 +258,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(await findByTestId('loading-indicator')).toBeVisible();
|
expect(await findByTestId('loading-indicator')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set FilterBar width by useStoredSidebarWidth', () => {
|
test('should set FilterBar width by useStoredSidebarWidth', () => {
|
||||||
const expectedValue = 200;
|
const expectedValue = 200;
|
||||||
const setter = jest.fn();
|
const setter = jest.fn();
|
||||||
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
|
||||||
@@ -274,7 +275,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
|
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filter panel state when featureflag is true', () => {
|
test('filter panel state when featureflag is true', () => {
|
||||||
window.featureFlags = {
|
window.featureFlags = {
|
||||||
[FeatureFlag.FilterBarClosedByDefault]: true,
|
[FeatureFlag.FilterBarClosedByDefault]: true,
|
||||||
};
|
};
|
||||||
@@ -294,7 +295,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`);
|
expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filter panel state when featureflag is false', () => {
|
test('filter panel state when featureflag is false', () => {
|
||||||
window.featureFlags = {
|
window.featureFlags = {
|
||||||
[FeatureFlag.FilterBarClosedByDefault]: false,
|
[FeatureFlag.FilterBarClosedByDefault]: false,
|
||||||
};
|
};
|
||||||
@@ -314,7 +315,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`);
|
expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render the filter bar when nativeFiltersEnabled is false', () => {
|
test('should not render the filter bar when nativeFiltersEnabled is false', () => {
|
||||||
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
||||||
showDashboard: true,
|
showDashboard: true,
|
||||||
missingInitialFilters: [],
|
missingInitialFilters: [],
|
||||||
@@ -327,7 +328,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
|
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the filter bar when nativeFiltersEnabled is true and not in edit mode', () => {
|
test('should render the filter bar when nativeFiltersEnabled is true and not in edit mode', () => {
|
||||||
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
||||||
showDashboard: true,
|
showDashboard: true,
|
||||||
missingInitialFilters: [],
|
missingInitialFilters: [],
|
||||||
@@ -340,7 +341,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(queryByTestId('dashboard-filters-panel')).toBeInTheDocument();
|
expect(queryByTestId('dashboard-filters-panel')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render the filter bar when in edit mode even if nativeFiltersEnabled is true', () => {
|
test('should not render the filter bar when in edit mode even if nativeFiltersEnabled is true', () => {
|
||||||
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
|
||||||
showDashboard: true,
|
showDashboard: true,
|
||||||
missingInitialFilters: [],
|
missingInitialFilters: [],
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ buildActiveFilters({
|
|||||||
components: dashboardWithFilter,
|
components: dashboardWithFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('for dashboard filters', () => {
|
describe('for dashboard filters', () => {
|
||||||
test('does not show number when there are no active filters', () => {
|
test('does not show number when there are no active filters', () => {
|
||||||
const store = getMockStoreWithFilters();
|
const store = getMockStoreWithFilters();
|
||||||
@@ -96,6 +97,7 @@ describe('for dashboard filters', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('for native filters', () => {
|
describe('for native filters', () => {
|
||||||
test('does not show number when there are no active filters', () => {
|
test('does not show number when there are no active filters', () => {
|
||||||
const store = getMockStoreWithNativeFilters();
|
const store = getMockStoreWithNativeFilters();
|
||||||
|
|||||||
@@ -86,16 +86,6 @@ const StyledFilterCount = styled.div`
|
|||||||
const StyledBadge = styled(Badge)`
|
const StyledBadge = styled(Badge)`
|
||||||
${({ theme }) => `
|
${({ theme }) => `
|
||||||
margin-left: ${theme.sizeUnit * 2}px;
|
margin-left: ${theme.sizeUnit * 2}px;
|
||||||
&>sup.ant-badge-count {
|
|
||||||
padding: 0 ${theme.sizeUnit}px;
|
|
||||||
min-width: ${theme.sizeUnit * 4}px;
|
|
||||||
height: ${theme.sizeUnit * 4}px;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: ${theme.fontWeightStrong};
|
|
||||||
font-size: ${theme.fontSizeSM - 1}px;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 0 ${theme.sizeUnit}px;
|
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -314,6 +304,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
|
|||||||
data-test="applied-filter-count"
|
data-test="applied-filter-count"
|
||||||
className="applied-count"
|
className="applied-count"
|
||||||
count={filterCount}
|
count={filterCount}
|
||||||
|
size="small"
|
||||||
showZero
|
showZero
|
||||||
/>
|
/>
|
||||||
</StyledFilterCount>
|
</StyledFilterCount>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ import { useHeaderActionsMenu } from './useHeaderActionsDropdownMenu';
|
|||||||
const extensionsRegistry = getExtensionsRegistry();
|
const extensionsRegistry = getExtensionsRegistry();
|
||||||
|
|
||||||
const headerContainerStyle = theme => css`
|
const headerContainerStyle = theme => css`
|
||||||
border-bottom: 1px solid ${theme.colorSplit};
|
border-bottom: 1px solid ${theme.colorBorder};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const editButtonStyle = theme => css`
|
const editButtonStyle = theme => css`
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ const setup = (overrides?: MissingChartProps) => (
|
|||||||
<MissingChart height={100} {...overrides} />
|
<MissingChart height={100} {...overrides} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('MissingChart', () => {
|
describe('MissingChart', () => {
|
||||||
it('renders a .missing-chart-container', () => {
|
test('renders a .missing-chart-container', () => {
|
||||||
const rendered = render(setup());
|
const rendered = render(setup());
|
||||||
|
|
||||||
const missingChartContainer = rendered.container.querySelector(
|
const missingChartContainer = rendered.container.querySelector(
|
||||||
@@ -38,7 +39,7 @@ describe('MissingChart', () => {
|
|||||||
expect(missingChartContainer).toBeVisible();
|
expect(missingChartContainer).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a .missing-chart-body', () => {
|
test('renders a .missing-chart-body', () => {
|
||||||
const rendered = render(setup());
|
const rendered = render(setup());
|
||||||
|
|
||||||
const missingChartBody = rendered.container.querySelector(
|
const missingChartBody = rendered.container.querySelector(
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ afterAll(() => {
|
|||||||
fetchMock.restore();
|
fetchMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('PropertiesModal', () => {
|
describe('PropertiesModal', () => {
|
||||||
jest.setTimeout(60000); // Increased timeout for complex modal rendering
|
jest.setTimeout(60000); // Increased timeout for complex modal rendering
|
||||||
|
|
||||||
@@ -354,9 +355,19 @@ describe('PropertiesModal', () => {
|
|||||||
mockedIsFeatureEnabled.mockReturnValue(false);
|
mockedIsFeatureEnabled.mockReturnValue(false);
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
props.onlyApply = false;
|
props.onlyApply = false;
|
||||||
render(<PropertiesModal {...props} />, {
|
// Pass dashboardInfo to avoid loading state
|
||||||
|
const propsWithDashboardInfo = {
|
||||||
|
...props,
|
||||||
|
dashboardInfo: {
|
||||||
|
...dashboardInfo,
|
||||||
|
json_metadata: mockedJsonMetadata,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<PropertiesModal {...propsWithDashboardInfo} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wait for the form to be visible
|
||||||
expect(
|
expect(
|
||||||
await screen.findByTestId('dashboard-edit-properties-form'),
|
await screen.findByTestId('dashboard-edit-properties-form'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -379,9 +390,19 @@ describe('PropertiesModal', () => {
|
|||||||
mockedIsFeatureEnabled.mockReturnValue(false);
|
mockedIsFeatureEnabled.mockReturnValue(false);
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
props.onlyApply = true;
|
props.onlyApply = true;
|
||||||
render(<PropertiesModal {...props} />, {
|
// Pass dashboardInfo to avoid loading state
|
||||||
|
const propsWithDashboardInfo = {
|
||||||
|
...props,
|
||||||
|
dashboardInfo: {
|
||||||
|
...dashboardInfo,
|
||||||
|
json_metadata: mockedJsonMetadata,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<PropertiesModal {...propsWithDashboardInfo} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wait for the form to be visible
|
||||||
expect(
|
expect(
|
||||||
await screen.findByTestId('dashboard-edit-properties-form'),
|
await screen.findByTestId('dashboard-edit-properties-form'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user