Compare commits

..

23 Commits

Author SHA1 Message Date
Elizabeth Thompson
3d2c332165 Merge remote-tracking branch 'origin/master' into docs/testing-guidelines-test-function 2025-09-29 13:13:49 -07:00
Elizabeth Thompson
572f3392d7 docs(testing): add guidelines for using test() instead of describe()/it()
Added comprehensive testing structure guidelines to LLMS.md that explain why and how to use test() instead of describe() and it(), following the "avoid nesting when testing" principle for better test isolation and readability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 10:25:15 -07:00
Geidō
90f281f585 fix: AceEditor Autocomplete Highlight (#35316) 2025-09-29 13:37:30 +03:00
Evan Rusackas
d62249d13f test(frontend): Migrate from describe/it to flat test() pattern (#35305)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-28 11:45:33 -07:00
Maxime Beauchemin
ff102aadb3 refactor(llm): rename LLMS.md to AGENTS.md for modern AI tools (#35314)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-27 12:46:16 +03:00
Elizabeth Thompson
82e2bc6181 fix(DatasourceModal): replace imperative modal updates with declarative state (#35256)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-26 17:54:17 -07:00
Gabriel Torres Ruiz
784ff82847 fix(sqllab): fix blank bottom section in SQL Lab left panel (#35309) 2025-09-26 20:07:20 +03:00
Mehmet Salih Yavuz
027b25e6b8 fix(DateFilterControl): remove modal overlay style to fix z-index issues (#35292) 2025-09-26 15:42:46 +02:00
SBIN2010
b652fab042 fix(table): New ad-hoc columns retain the name of previous columns (#35274) 2025-09-26 10:34:55 -03:00
Nikita Rybalchenko
77a5969dc1 feat(pdf): add configurable PDF compression level support (#34096) 2025-09-25 08:29:54 -07:00
Geidō
fb9032c05c fix: Cosmetic issues (#35122) 2025-09-25 17:24:34 +03:00
Mehmet Salih Yavuz
7a9dbfe879 fix(BuilderComponentPane): navigation tabs padding (#35213) 2025-09-25 16:59:48 +03:00
Giulio Piccolo
0de78d8203 fix(deck.gl): ensure min/max values are included in polygon map legend breakpoints (#35033)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2025-09-25 14:30:44 +03:00
Maxime Beauchemin
abc2d46fed refactor: remove obsolete Flask flash messaging system (#35237)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-25 00:05:16 -07:00
dependabot[bot]
927cc1cda1 chore(deps): bump tar-fs from 3.1.0 to 3.1.1 in /superset-frontend (#35272)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 22:29:13 -07:00
JUST.in DO IT
7f3840557a chore(react18): Migrate legacy react methods (#34892) 2025-09-24 12:34:22 -07:00
JUST.in DO IT
0defcb604b chore(sqllab): remove unused json param (#35065) 2025-09-24 10:26:55 -07:00
Beto Dealmeida
94686ddfbe fix(SQL Lab): syncTable on new tabs (#35216) 2025-09-24 11:58:54 -04:00
SBIN2010
ec322dfd8d fix(Mixed Chart): Tooltip incorrectly displays numbers with optional Y-axis format and showQueryIdentifiers set to true (#35224) 2025-09-24 17:44:01 +03:00
Mehmet Salih Yavuz
cb88d886c7 fix(PropertiesModal): do not show validation errors while loading (#35215) 2025-09-24 10:52:16 +03:00
Maxime Beauchemin
608e3baf43 feat(build): auto-rebuild/check TypeScript types for packages/plugins in webpack (#35240)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 19:22:59 -07:00
Elizabeth Thompson
b6f6b75348 fix(dashboard): update header border to use colorBorder token (#35199)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 18:08:17 -07:00
Elizabeth Thompson
a5ad1d186c docs: Add instruction to avoid time-specific language in code comments (#35200)
Co-authored-by: Claude <noreply@anthropic.com>
2025-09-23 18:07:59 -07:00
335 changed files with 4077 additions and 3397 deletions

View File

@@ -1 +1 @@
../LLMS.md ../AGENTS.md

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
LLMS.md AGENTS.md

View File

@@ -1 +1 @@
LLMS.md AGENTS.md

2
GPT.md
View File

@@ -1 +1 @@
LLMS.md AGENTS.md

View File

@@ -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,

View File

@@ -3,3 +3,4 @@ cypress/screenshots
cypress/videos cypress/videos
src/temp src/temp
.temp_cache/ .temp_cache/
.tsbuildinfo

View File

@@ -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",

View File

@@ -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);
}
});
});
});

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View 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"]
}
}
]
}

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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,
),
); );
} }

View File

@@ -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(

View File

@@ -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(

View File

@@ -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({

View File

@@ -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();

View File

@@ -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

View File

@@ -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, {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();

View File

@@ -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 },

View File

@@ -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(

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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(

View File

@@ -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`

View File

@@ -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

View File

@@ -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(),
); );

View File

@@ -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');

View File

@@ -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} />, {

View File

@@ -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(
{}, {},

View File

@@ -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: {

View File

@@ -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,

View File

@@ -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');

View File

@@ -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` },

View File

@@ -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' },
}; };

View File

@@ -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}
> >

View File

@@ -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,

View File

@@ -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>
</>
);
};

View File

@@ -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

View File

@@ -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();
});

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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';

View File

@@ -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

View File

@@ -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(

View File

@@ -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;
};
}; };

View File

@@ -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);
}); });

View File

@@ -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'));

View File

@@ -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();
});

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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,

View File

@@ -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([]);

View File

@@ -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();

View File

@@ -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,

View File

@@ -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');

View File

@@ -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 },

View File

@@ -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();
});
}); });

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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');

View File

@@ -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"`,

View File

@@ -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: [

View File

@@ -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);
});

View File

@@ -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 };

View File

@@ -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];

View File

@@ -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 });

View File

@@ -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(),

View File

@@ -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' }]);
});
}); });

View File

@@ -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>
); );
} }

View File

@@ -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');
});
}); });

View File

@@ -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';

View File

@@ -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
>; >;

View File

@@ -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();

View File

@@ -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 = [

View File

@@ -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';

View File

@@ -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 = {

View File

@@ -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 },
}); });

View File

@@ -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 = {

View File

@@ -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:

View File

@@ -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 },

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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: [],

View File

@@ -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();

View File

@@ -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>

View File

@@ -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`

View File

@@ -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(

View File

@@ -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