Compare commits

...

25 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
Beto Dealmeida
db88d80b3f fix: docker-compose-image-tag (#35246) 2025-09-23 14:57:53 -07:00
Rafael Benitez
4b71adaa9c feat(themes): Adding SupersetText support to Themes Modal (#35248) 2025-09-23 22:23:57 +02:00
335 changed files with 4082 additions and 3166 deletions

View File

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

View File

@@ -82,6 +82,7 @@ intro_header.txt
# for LLMs
llm-context.md
AGENTS.md
LLMS.md
CLAUDE.md
CURSOR.md

View File

@@ -68,7 +68,11 @@ superset/
### Apache License Headers
- **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
@@ -98,6 +102,17 @@ superset/
- **`selectOption()`** - Select component helper
- **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
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
- **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

@@ -28,6 +28,7 @@ x-superset-image: &superset-image apachesuperset.docker.scarf.sh/apache/superset
x-superset-volumes:
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
- ./docker:/app/docker
- ./superset-core:/app/superset-core
- superset_home:/app/superset_home
services:

View File

@@ -413,13 +413,6 @@ module.exports = {
'icons/no-fa-icons-usage': 'error',
'i18n-strings/no-template-vars': ['error', true],
'i18n-strings/sentence-case-buttons': 'error',
camelcase: [
'error',
{
allow: ['^UNSAFE_'],
properties: 'never',
},
],
'class-methods-use-this': 0,
curly: 2,
'func-names': 0,

View File

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

View File

@@ -8886,9 +8886,9 @@
"license": "ISC"
},
"node_modules/@ndelangen/get-tarball/node_modules/tar-fs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -60740,7 +60740,7 @@
},
"packages/superset-core": {
"name": "@apache-superset/core",
"version": "0.0.1-rc4",
"version": "0.0.1-rc5",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.26.4",

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getColorBreakpointsBuckets } from './utils';
import { getColorBreakpointsBuckets, getBreakPoints } from './utils';
import { ColorBreakpointType } from './types';
describe('getColorBreakpointsBuckets', () => {
@@ -44,3 +44,447 @@ describe('getColorBreakpointsBuckets', () => {
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) {
return [];
}
// Handle Infinity values
if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) {
return [];
}
const delta = (maxValue - minValue) / numBuckets;
const precision =
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)
.fill(0)
.map((_, i) => (startValue + i * delta).toFixed(precision));
// Generate breakpoints
const breakPoints = new Array(numBuckets + 1).fill(0).map((_, i) => {
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(
@@ -146,7 +162,10 @@ export function getBreakPointColorScaler(
scaler = scaleThreshold<number, string>()
.domain(points)
.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 {
// interpolate colors linearly
const linearScaleDomain = extent(features, accessor);

View File

@@ -322,12 +322,6 @@ export default function transformProps(
primarySeries.add(seriesOption.id as string);
}
};
rawSeriesA.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndex),
);
rawSeriesB.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndexB),
);
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
stack,
onlyTotal,
@@ -460,7 +454,11 @@ export default function transformProps(
theme,
},
);
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
series.push(transformedSeries);
mapSeriesIdToAxis(transformedSeries, yAxisIndex);
}
});
rawSeriesB.forEach(entry => {
@@ -528,7 +526,11 @@ export default function transformProps(
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

View File

@@ -49,7 +49,6 @@ export default {
dash_edit_perm: true,
dash_save_perm: true,
common: {
flash_messages: [],
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
},
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 = {
client_id: query.id,
database_id: query.dbId,
json: true,
runAsync: query.runAsync,
catalog: query.catalog,
schema: query.schema,
@@ -956,7 +955,7 @@ export function addTable(queryEditor, tableName, catalogName, schemaName) {
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const table = {
dbId,
queryEditorId: queryEditor.id,
queryEditorId: queryEditor.tabViewId ?? queryEditor.id,
catalog: catalogName,
schema: schemaName,
name: tableName,

View File

@@ -49,6 +49,7 @@ jest.mock('@superset-ui/core', () => ({
isFeatureEnabled: jest.fn(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getUpToDateQuery', () => {
test('should return the up to date query editor state', () => {
const outOfUpdatedQueryEditor = {
@@ -72,6 +73,7 @@ describe('getUpToDateQuery', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('async actions', () => {
const mockBigNumber = '9223372036854775807';
const queryEditor = {
@@ -100,6 +102,7 @@ describe('async actions', () => {
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveQuery', () => {
const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
fetchMock.post(saveQueryEndpoint, { results: { json: {} } });
@@ -109,7 +112,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState);
};
it('posts to the correct url', () => {
test('posts to the correct url', () => {
expect.assertions(1);
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);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
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);
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);
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);
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', () => {
const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
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', () => {
const makeRequest = () => {
const store = mockStore(initialState);
@@ -186,7 +191,7 @@ describe('async actions', () => {
return request(dispatch, store.getState);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -194,7 +199,7 @@ describe('async actions', () => {
});
});
it('calls requestQueryResults', () => {
test('calls requestQueryResults', () => {
expect.assertions(1);
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(() => {
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
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);
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);
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', () => {
const makeRequest = () => {
const request = actions.runQuery(query);
return request(dispatch, () => initialState);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -262,7 +268,7 @@ describe('async actions', () => {
});
});
it('calls startQuery', () => {
test('calls startQuery', () => {
expect.assertions(1);
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(() => {
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
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);
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);
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', () => {
const { location } = window;
@@ -342,7 +349,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState);
};
it('makes the fetch request', async () => {
test('makes the fetch request', async () => {
const runQueryEndpointWithParams =
'glob:*/api/v1/sqllab/execute/?foo=bar';
fetchMock.post(
@@ -355,8 +362,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('reRunQuery', () => {
it('creates new query with a new id', () => {
test('creates new query with a new id', () => {
const id = 'id';
const state = {
sqlLab: {
@@ -372,6 +380,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('postStopQuery', () => {
const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
fetchMock.post(stopQueryEndpoint, {});
@@ -385,7 +394,7 @@ describe('async actions', () => {
return request(dispatch);
};
it('makes the fetch request', () => {
test('makes the fetch request', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -393,7 +402,7 @@ describe('async actions', () => {
});
});
it('calls stopQuery', () => {
test('calls stopQuery', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -401,7 +410,7 @@ describe('async actions', () => {
});
});
it('sends the correct data', () => {
test('sends the correct data', () => {
expect.assertions(1);
return makeRequest().then(() => {
@@ -412,8 +421,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('cloneQueryToNewTab', () => {
it('creates new query editor', () => {
test('creates new query editor', () => {
expect.assertions(1);
const id = 'id';
@@ -455,6 +465,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('popSavedQuery', () => {
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
const store = mockStore({});
@@ -508,7 +519,7 @@ describe('async actions', () => {
supersetClientGetSpy.mockRestore();
});
it('calls API endpint with correct params', async () => {
test('calls API endpint with correct params', async () => {
supersetClientGetSpy.mockResolvedValue({
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({
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());
await makeRequest(1);
@@ -561,8 +572,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
it('creates new query editor', () => {
test('creates new query editor', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -582,8 +594,9 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addNewQueryEditor', () => {
it('creates new query editor with new tab name', () => {
test('creates new query editor with new tab name', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -621,7 +634,7 @@ describe('async actions', () => {
});
});
it('set current query editor', () => {
test('set current query editor', () => {
expect.assertions(1);
const store = mockStore(initialState);
@@ -636,8 +649,9 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('swithQueryEditor', () => {
it('switch to the next tab editor', () => {
test('switch to the next tab editor', () => {
const store = mockStore(initialState);
const expectedActions = [
{
@@ -650,7 +664,7 @@ describe('async actions', () => {
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({
...initialState,
sqlLab: {
@@ -673,7 +687,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions);
});
it('switch to the previous tab editor', () => {
test('switch to the previous tab editor', () => {
const store = mockStore({
...initialState,
sqlLab: {
@@ -692,7 +706,7 @@ describe('async actions', () => {
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({
...initialState,
sqlLab: {
@@ -715,6 +729,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('backend sync', () => {
const updateTabStateEndpoint = 'glob:*/tabstateview/*';
fetchMock.put(updateTabStateEndpoint, {});
@@ -745,8 +760,9 @@ describe('async actions', () => {
afterEach(() => fetchMock.resetHistory());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
it('creates the tab state in the local storage', () => {
test('creates the tab state in the local storage', () => {
expect.assertions(2);
const store = mockStore({});
@@ -769,8 +785,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('removeQueryEditor', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const store = mockStore({});
@@ -785,8 +802,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetDb', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const dbId = 42;
@@ -803,8 +821,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetCatalog', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const catalog = 'public';
@@ -821,8 +840,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetSchema', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const schema = 'schema';
@@ -839,8 +859,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAutorun', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const autorun = true;
@@ -857,8 +878,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTitle', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const name = 'name';
@@ -877,6 +899,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAndSaveSql', () => {
const sql = 'SELECT * ';
const expectedActions = [
@@ -886,8 +909,9 @@ describe('async actions', () => {
sql,
},
];
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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);
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', () => {
it('does not update the tab state in the backend', () => {
test('does not update the tab state in the backend', () => {
isFeatureEnabled.mockImplementation(
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', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
const queryLimit = 10;
@@ -947,8 +973,9 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTemplateParams', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(1);
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', () => {
it('dispatches table state from unsaved change', () => {
test('dispatches table state from unsaved change', () => {
const tableName = 'table';
const catalogName = null;
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', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(4);
const tableName = 'table';
@@ -1028,6 +1136,7 @@ describe('async actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runTablePreviewQuery', () => {
const results = {
data: mockBigNumber,
@@ -1058,7 +1167,7 @@ describe('async actions', () => {
fetchMock.resetHistory();
});
it('updates and runs data preview query when configured', () => {
test('updates and runs data preview query when configured', () => {
expect.assertions(3);
const expectedActionTypes = [
@@ -1082,7 +1191,7 @@ describe('async actions', () => {
});
});
it('runs data preview query only', () => {
test('runs data preview query only', () => {
const expectedActionTypes = [
actions.START_QUERY, // runQuery (data preview)
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', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
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', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
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', () => {
it('updates the table schema state in the backend', () => {
test('updates the table schema state in the backend', () => {
expect.assertions(2);
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);
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);
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', () => {
it('updates the tab state in the backend', () => {
test('updates the tab state in the backend', () => {
expect.assertions(3);
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', () => {
beforeEach(() => {
(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 { getByTestId } = setup(defaultQueryEditor, store);
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 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);
setup(defaultQueryEditor, store);

View File

@@ -65,6 +65,7 @@ const AceEditorWrapper = ({
'catalog',
'schema',
'templateParams',
'tabViewId',
]);
// Prevent a maximum update depth exceeded error
// by skipping access the unsaved query editor state
@@ -172,6 +173,7 @@ const AceEditorWrapper = ({
dbId: queryEditor.dbId,
catalog: queryEditor.catalog,
schema: queryEditor.schema,
tabViewId: queryEditor.tabViewId,
},
!autocomplete,
);
@@ -191,6 +193,11 @@ const AceEditorWrapper = ({
width: ${theme.sizeUnit * 130}px !important;
}
.ace_completion-highlight {
color: ${theme.colorPrimaryText} !important;
background-color: ${theme.colorPrimaryBgHover};
}
.ace_tooltip {
max-width: ${SQL_EDITOR_LEFTBAR_WIDTH}px;
}

View File

@@ -44,6 +44,7 @@ type Params = {
dbId?: string | number;
catalog?: string | null;
schema?: string;
tabViewId?: string;
};
const EMPTY_LIST = [] as typeof sqlKeywords;
@@ -59,7 +60,7 @@ const getHelperText = (value: string) =>
const extensionsRegistry = getExtensionsRegistry();
export function useKeywords(
{ queryEditorId, dbId, catalog, schema }: Params,
{ queryEditorId, dbId, catalog, schema, tabViewId }: Params,
skip = false,
) {
const useCustomKeywords = extensionsRegistry.get(
@@ -147,7 +148,12 @@ export function useKeywords(
const insertMatch = useEffectEvent((editor: Editor, data: any) => {
if (data.meta === 'table') {
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;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlLab App', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
@@ -60,23 +61,23 @@ describe('SqlLab App', () => {
jest.useRealTimers();
});
it('is valid', () => {
test('is valid', () => {
expect(isValidElement(<App />)).toBe(true);
});
it('should render', () => {
test('should render', () => {
const { getByTestId } = render(<App />, { useRedux: true, store });
expect(getByTestId('SqlLabApp')).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 });
unmount();
expect(Mousetrap.reset).toHaveBeenCalled();
});
it('logs current usage warning', () => {
test('logs current usage warning', () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
const initialState = {
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 storeExceedLocalStorage = mockStore(
sqlLabReducer(

View File

@@ -21,22 +21,23 @@ import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ColumnElement', () => {
const mockedProps = {
actions: mockedActions,
column: table.columns[0],
};
it('is valid with props', () => {
test('is valid with props', () => {
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]} />);
expect(container.querySelector('i.fa-key')).toBeInTheDocument();
expect(
container.querySelector('[data-test="col-name"]')?.firstChild,
).toHaveTextContent('id');
});
it('renders a multi-key column', () => {
test('renders a multi-key column', () => {
const { container } = render(<ColumnElement column={table.columns[1]} />);
expect(container.querySelector('i.fa-link')).toBeInTheDocument();
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
@@ -44,7 +45,7 @@ describe('ColumnElement', () => {
container.querySelector('[data-test="col-name"]')?.firstChild,
).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]} />);
expect(container.querySelector('i')).not.toBeInTheDocument();
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', () => {
it('renders EstimateQueryCostButton', async () => {
test('renders EstimateQueryCostButton', async () => {
const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Estimate cost')).toBeInTheDocument();
});
it('renders label for selected query', async () => {
test('renders label for selected query', async () => {
const { queryByText } = setup(
{ queryEditorId: extraQueryEditor1.id },
mockStore(initialState),
@@ -69,7 +70,7 @@ describe('EstimateQueryCostButton', () => {
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(
{},
mockStore({
@@ -87,7 +88,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
});
it('renders estimation error result', async () => {
test('renders estimation error result', async () => {
const { queryByText, getByText } = setup(
{},
mockStore({
@@ -109,7 +110,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate error')).toBeInTheDocument();
});
it('renders estimation success result', async () => {
test('renders estimation success result', async () => {
const { queryByText, getByText } = setup(
{},
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', () => {
const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm');
postFormSpy.mockImplementation(jest.fn());
it('renders', async () => {
test('renders', async () => {
const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Explore')).toBeInTheDocument();
});
it('visualize results', async () => {
test('visualize results', async () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
@@ -75,7 +76,7 @@ describe('ExploreCtasResultsButton', () => {
});
});
it('visualize results fails', async () => {
test('visualize results fails', async () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();

View File

@@ -31,8 +31,9 @@ const setup = (
useRedux: true,
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ExploreResultsButton', () => {
it('renders', async () => {
test('renders', async () => {
const { queryByText } = setup(jest.fn(), {
database: { allows_subquery: true },
});
@@ -41,7 +42,7 @@ describe('ExploreResultsButton', () => {
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());
expect(queryByText('Create chart')).toBeInTheDocument();
// Updated line to match the actual button name that includes the icon

View File

@@ -43,6 +43,7 @@ const mockState = {
databases: mockDatabases,
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryAutoRefresh', () => {
const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery };
const successfulQueries: QueryDictionary = {
@@ -62,15 +63,15 @@ describe('QueryAutoRefresh', () => {
jest.useRealTimers();
});
it('isQueryRunning returns true for valid running query', () => {
test('isQueryRunning returns true for valid running query', () => {
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);
});
it('isQueryRunning returns false for invalid query', () => {
test('isQueryRunning returns false for invalid query', () => {
// @ts-ignore
expect(isQueryRunning(null)).toBe(false);
// @ts-ignore
@@ -81,15 +82,15 @@ describe('QueryAutoRefresh', () => {
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);
});
it('shouldCheckForQueries is false for valid completed query', () => {
test('shouldCheckForQueries is false for valid completed query', () => {
expect(shouldCheckForQueries(successfulQueries)).toBe(false);
});
it('shouldCheckForQueries is false for invalid inputs', () => {
test('shouldCheckForQueries is false for invalid inputs', () => {
// @ts-ignore
expect(shouldCheckForQueries(null)).toBe(false);
// @ts-ignore
@@ -109,7 +110,7 @@ describe('QueryAutoRefresh', () => {
).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 } });
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 } });
fetchMock.get(refreshApi, { result: [] });
@@ -164,7 +165,7 @@ describe('QueryAutoRefresh', () => {
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 } });
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 } });
fetchMock.get(refreshApi, {
@@ -219,7 +220,7 @@ describe('QueryAutoRefresh', () => {
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 } });
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', () => {
it('renders current query limit size', () => {
test('renders current query limit size', () => {
const queryLimit = 10;
const { getByText } = setup(
{
@@ -81,12 +82,12 @@ describe('QueryLimitSelect', () => {
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));
expect(getByText(defaultQueryLimit)).toBeInTheDocument();
});
it('renders queryLimit from unsavedQueryEditor', () => {
test('renders queryLimit from unsavedQueryEditor', () => {
const queryLimit = 10000;
const { getByText } = setup(
{},
@@ -104,7 +105,7 @@ describe('QueryLimitSelect', () => {
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
});
it('renders dropdown select', async () => {
test('renders dropdown select', async () => {
const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 50000 },
mockStore(initialState),
@@ -126,7 +127,7 @@ describe('QueryLimitSelect', () => {
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(
{ maxRow: 5 },
mockStore(initialState),
@@ -146,7 +147,7 @@ describe('QueryLimitSelect', () => {
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(
{ maxRow: 10000 },
mockStore(initialState),
@@ -168,7 +169,7 @@ describe('QueryLimitSelect', () => {
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 expectedIndex = 1;
const { baseElement, getAllByRole, getByRole } = setup({}, store);

View File

@@ -29,6 +29,7 @@ const mockedProps = {
latestQueryId: 'ryhMUZCGb',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryTable', () => {
test('is valid', () => {
expect(isValidElement(<QueryTable displayLimit={100} />)).toBe(true);

View File

@@ -150,6 +150,7 @@ const setup = (props?: any, store?: Store) =>
...(store && { store }),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ResultSet', () => {
beforeAll(() => {
setupAGGridModules();

View File

@@ -53,17 +53,17 @@ const setup = (props?: Partial<RunQueryActionButtonProps>, store?: Store) =>
...(store && { store }),
});
it('renders a single Button', () => {
test('renders a single Button', () => {
const { getByRole } = setup({}, mockStore(initialState));
expect(getByRole('button')).toBeInTheDocument();
});
it('renders a label for Run Query', () => {
test('renders a label for Run Query', () => {
const { getByText } = setup({}, mockStore(initialState));
expect(getByText('Run')).toBeInTheDocument();
});
it('renders a label for Selected Query', () => {
test('renders a label for Selected Query', () => {
const { getByText } = setup(
{},
mockStore({
@@ -80,7 +80,7 @@ it('renders a label for Selected Query', () => {
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(
{},
mockStore({
@@ -98,7 +98,7 @@ it('disable button when sql from unsaved changes is empty', () => {
expect(button).toBeDisabled();
});
it('disable button when selectedText only contains blank contents', () => {
test('disable button when selectedText only contains blank contents', () => {
const { getByRole } = setup(
{},
mockStore({
@@ -116,7 +116,7 @@ it('disable button when selectedText only contains blank contents', () => {
expect(button).toBeDisabled();
});
it('enable default button for unrelated unsaved changes', () => {
test('enable default button for unrelated unsaved changes', () => {
const { getByRole } = setup(
{},
mockStore({
@@ -134,7 +134,7 @@ it('enable default button for unrelated unsaved changes', () => {
expect(button).toBeEnabled();
});
it('dispatch runQuery on click', async () => {
test('dispatch runQuery on click', async () => {
const runQuery = jest.fn();
const { getByRole } = setup({ runQuery }, mockStore(initialState));
const button = getByRole('button');
@@ -143,7 +143,7 @@ it('dispatch runQuery on click', async () => {
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 { getByRole } = setup(
{ queryState: 'running', stopQuery },

View File

@@ -24,6 +24,7 @@ const overlayMenu = (
<Menu items={[{ label: 'Save dataset', key: 'save-dataset' }]} />
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetActionButton', () => {
test('renders a split save button', async () => {
render(

View File

@@ -62,8 +62,9 @@ jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetModal', () => {
it('renders a "Save as new" field', () => {
test('renders a "Save as new" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const saveRadioBtn = screen.getByRole('radio', {
@@ -80,7 +81,7 @@ describe('SaveDatasetModal', () => {
expect(inputFieldText).toBeInTheDocument();
});
it('renders an "Overwrite existing" field', () => {
test('renders an "Overwrite existing" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const overwriteRadioBtn = screen.getByRole('radio', {
@@ -96,20 +97,20 @@ describe('SaveDatasetModal', () => {
expect(placeholderText).toBeInTheDocument();
});
it('renders a close button', () => {
test('renders a close button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
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 });
// "Save as new" is selected when the modal opens by default
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 });
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons
@@ -123,7 +124,7 @@ describe('SaveDatasetModal', () => {
).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 });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -155,7 +156,7 @@ describe('SaveDatasetModal', () => {
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 });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -196,7 +197,7 @@ describe('SaveDatasetModal', () => {
).toBeInTheDocument();
});
it('sends the schema when creating the dataset', async () => {
test('sends the schema when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch);
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({});
useDispatchMock.mockReturnValue(dummyDispatch);
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 });
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
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,
@@ -266,7 +267,7 @@ describe('SaveDatasetModal', () => {
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
global.featureFlags = {
[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
global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true,

View File

@@ -64,8 +64,9 @@ const splitSaveBtnProps = {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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 = {
...mockedProps,
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} />, {
useRedux: true,
store: mockStore(mockState),
@@ -95,7 +96,7 @@ describe('SavedQuery', () => {
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} />, {
useRedux: true,
store: mockStore(mockState),
@@ -111,7 +112,7 @@ describe('SavedQuery', () => {
expect(saveQueryModalHeader).toBeInTheDocument();
});
it('renders the save query modal UI', () => {
test('renders the save query modal UI', () => {
render(<SaveQuery {...mockedProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -146,7 +147,7 @@ describe('SavedQuery', () => {
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} />, {
useRedux: true,
store: mockStore({
@@ -171,7 +172,7 @@ describe('SavedQuery', () => {
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} />, {
useRedux: true,
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} />, {
useRedux: true,
store: mockStore(mockState),
@@ -205,7 +206,7 @@ describe('SavedQuery', () => {
expect(saveDatasetHeader).toBeInTheDocument();
});
it('renders the save dataset modal UI', async () => {
test('renders the save dataset modal UI', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true,
store: mockStore(mockState),
@@ -246,7 +247,7 @@ describe('SavedQuery', () => {
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;
const savePromise = new Promise<void>(resolve => {
resolveSave = resolve;
@@ -290,7 +291,7 @@ describe('SavedQuery', () => {
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);
// Mock state for a new tab with default SQL

View File

@@ -76,6 +76,7 @@ const unsavedQueryEditor = {
templateParams: '{ "my_value": "foo" }',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ShareSqlLabQuery', () => {
const storeQueryUrl = 'glob:*/api/v1/sqllab/permalink';
const storeQueryMockId = 'ci39c3';
@@ -94,6 +95,7 @@ describe('ShareSqlLabQuery', () => {
afterAll(() => fetchMock.reset());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('via permalink api', () => {
beforeAll(() => {
mockedIsFeatureEnabled.mockImplementation(() => true);
@@ -103,7 +105,7 @@ describe('ShareSqlLabQuery', () => {
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 () => {
render(<ShareSqlLabQuery {...defaultProps} />, {
useRedux: true,
@@ -121,7 +123,7 @@ describe('ShareSqlLabQuery', () => {
).toEqual(expected);
});
it('calls storeQuery() with unsaved changes', async () => {
test('calls storeQuery() with unsaved changes', async () => {
await act(async () => {
render(<ShareSqlLabQuery {...defaultProps} />, {
useRedux: true,

View File

@@ -149,6 +149,7 @@ const createStore = (initState: object) =>
getDefaultMiddleware().concat(api.middleware, logAction),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditor', () => {
beforeAll(() => {
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 { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect(
@@ -200,7 +201,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
it('renders db unavailable message', async () => {
test('renders db unavailable message', async () => {
const queryEditor = initialState.sqlLab.queryEditors[1];
const { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect(
@@ -210,7 +211,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
it('render a SqlEditorLeftBar', async () => {
test('render a SqlEditorLeftBar', async () => {
const { getByTestId, unmount } = setup(mockedProps, store);
await waitFor(
@@ -222,7 +223,7 @@ describe('SqlEditor', () => {
}, 15000);
// Update other similar tests with timeouts
it('render an AceEditorWrapper', async () => {
test('render an AceEditorWrapper', async () => {
const { findByTestId, unmount } = setup(mockedProps, store);
await waitFor(
@@ -233,7 +234,7 @@ describe('SqlEditor', () => {
unmount();
}, 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(
{
...mockedProps,
@@ -245,7 +246,7 @@ describe('SqlEditor', () => {
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 editor = await findByTestId('react-ace');
const sql = 'select *';
@@ -260,7 +261,7 @@ describe('SqlEditor', () => {
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';
store = createStore({
...initialState,
@@ -293,12 +294,12 @@ describe('SqlEditor', () => {
expect(editor).toHaveValue(expectedSql);
});
it('render a SouthPane', async () => {
test('render a SouthPane', async () => {
const { findByTestId } = setup(mockedProps, store);
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({
...initialState,
sqlLab: {
@@ -338,7 +339,7 @@ describe('SqlEditor', () => {
);
});
it('render a Limit Dropdown', async () => {
test('render a Limit Dropdown', async () => {
const defaultQueryLimit = 101;
const updatedProps = { ...mockedProps, defaultQueryLimit };
const { findByText } = setup(updatedProps, store);
@@ -346,7 +347,7 @@ describe('SqlEditor', () => {
expect(await findByText('10 000')).toBeInTheDocument();
});
it('renders an Extension if provided', async () => {
test('renders an Extension if provided', async () => {
const extensionsRegistry = getExtensionsRegistry();
extensionsRegistry.set('sqleditor.extension.form', () => (
@@ -360,6 +361,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with EstimateQueryCost enabled', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(
@@ -370,7 +372,7 @@ describe('SqlEditor', () => {
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/';
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', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(
@@ -446,7 +449,7 @@ describe('SqlEditor', () => {
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`;
fetchMock.post(switchTabApi, {});
const { getByTestId } = setup(

View File

@@ -168,6 +168,9 @@ const StyledToolbar = styled.div`
const StyledSidebar = styled.div`
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
height: 100%;
display: flex;
flex-direction: column;
`;
const StyledSqlEditor = styled.div`

View File

@@ -47,7 +47,6 @@ import TableElement from '../TableElement';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
height?: number;
database?: DatabaseObject;
}
@@ -72,7 +71,6 @@ const LeftBarStyles = styled.div`
const SqlEditorLeftBar = ({
database,
queryEditorId,
height = 500,
}: SqlEditorLeftBarProps) => {
const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
({ sqlLab }) =>
@@ -84,6 +82,7 @@ const SqlEditorLeftBar = ({
'dbId',
'catalog',
'schema',
'tabViewId',
]);
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
@@ -170,7 +169,6 @@ const SqlEditorLeftBar = ({
};
const shouldShowReset = window.location.search === '?reset=1';
const tableMetaDataHeight = height - 130; // 130 is the height of the selects above
const handleCatalogChange = useCallback(
(catalog: string | null) => {
@@ -227,22 +225,16 @@ const SqlEditorLeftBar = ({
/>
<div className="divider" />
<StyledScrollbarContainer>
<div
css={css`
height: ${tableMetaDataHeight}px;
`}
>
{tables.map(table => (
<TableElement
table={table}
key={table.id}
activeKey={tables
.filter(({ expanded }) => expanded)
.map(({ id }) => id)}
onChange={onToggleTable}
/>
))}
</div>
{tables.map(table => (
<TableElement
table={table}
key={table.id}
activeKey={tables
.filter(({ expanded }) => expanded)
.map(({ id }) => id)}
onChange={onToggleTable}
/>
))}
</StyledScrollbarContainer>
{shouldShowReset && (
<Button

View File

@@ -56,15 +56,16 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
...(store && { store }),
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditorTabHeader', () => {
it('renders name', () => {
test('renders name', () => {
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument();
expect(queryByText(extraQueryEditor1.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 { queryByText } = setup(
defaultQueryEditor,
@@ -85,7 +86,7 @@ describe('SqlEditorTabHeader', () => {
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 { queryByText } = setup(
defaultQueryEditor,
@@ -106,6 +107,7 @@ describe('SqlEditorTabHeader', () => {
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with dropdown menus', () => {
let store = mockStore();
beforeEach(async () => {
@@ -116,7 +118,7 @@ describe('SqlEditorTabHeader', () => {
userEvent.click(dropdown);
});
it('should dispatch removeQueryEditor action', async () => {
test('should dispatch removeQueryEditor action', async () => {
await waitFor(() =>
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(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);
@@ -155,7 +157,7 @@ describe('SqlEditorTabHeader', () => {
mockPrompt.mockClear();
});
it('should dispatch toggleLeftBar action', async () => {
test('should dispatch toggleLeftBar action', async () => {
await waitFor(() =>
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(() =>
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(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
);

View File

@@ -56,6 +56,7 @@ afterEach(() => {
pathStub.mockReset();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('componentDidMount', () => {
let uriStub = jest.spyOn(URI.prototype, 'search');
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', () => {
test('refreshes table metadata when triggered', async () => {
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', () => {
it('should render with a title', () => {
test('should render with a title', () => {
const { container } = setup();
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();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
@@ -78,7 +79,7 @@ describe('TemplateParamsEditor', () => {
});
});
it('renders templateParams', async () => {
test('renders templateParams', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
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 { container, getByTestId } = setup(
{},

View File

@@ -52,23 +52,25 @@ const apiDataWithTabState = {
latest_query: null,
},
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getInitialState', () => {
afterEach(() => {
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);
});
it('should return undefined instead of null for templateParams', () => {
test('should return undefined instead of null for templateParams', () => {
expect(
getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0]
?.templateParams,
).toBeUndefined();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dedupeTabHistory', () => {
it('should dedupe the tab history', () => {
test('should dedupe the tab history', () => {
[
{ value: [], expected: [] },
{
@@ -136,8 +138,9 @@ describe('getInitialState', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dedupe tables schema', () => {
it('should dedupe the table schema', () => {
test('should dedupe the table schema', () => {
localStorage.setItem(
'redux',
JSON.stringify({
@@ -195,7 +198,7 @@ describe('getInitialState', () => {
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 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', () => {
const lastUpdatedTime = Date.now();
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 = {
...apiData,
active_tab: {
@@ -321,7 +325,7 @@ describe('getInitialState', () => {
).toEqual(apiDataWithTabState.active_tab.id.toString());
});
it('skip unsaved changes for expired data', () => {
test('skip unsaved changes for expired data', () => {
const apiDataWithLocalStorage = {
...apiData,
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 = {
...apiData,
active_tab: {

View File

@@ -23,7 +23,9 @@ import { table, initialState as mockState } from '../fixtures';
const initialState = mockState.sqlLab;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('sqlLabReducer', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Query editors actions', () => {
let newState;
let defaultQueryEditor;
@@ -38,12 +40,12 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, action);
qe = newState.queryEditors.find(e => e.id === 'abcd');
});
it('should add a query editor', () => {
test('should add a query editor', () => {
expect(newState.queryEditors).toHaveLength(
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 updateAction = {
type: actions.QUERY_EDITOR_SET_TITLE,
@@ -62,7 +64,7 @@ describe('sqlLabReducer', () => {
newState.queryEditors[newState.queryEditors.length - 1].id,
).toEqual('efgh');
});
it('should remove a query editor', () => {
test('should remove a query editor', () => {
expect(newState.queryEditors).toHaveLength(
initialState.queryEditors.length + 1,
);
@@ -75,7 +77,7 @@ describe('sqlLabReducer', () => {
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];
newState = {
...initialState,
@@ -94,7 +96,7 @@ describe('sqlLabReducer', () => {
);
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(
initialState.queryEditors.length + 1,
);
@@ -116,7 +118,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.dbId).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 addQueryEditorAction = {
type: actions.ADD_QUERY_EDITOR,
@@ -139,7 +141,7 @@ describe('sqlLabReducer', () => {
);
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 action = {
type: actions.QUERY_EDITOR_SETDB,
@@ -150,7 +152,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
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 action = {
type: actions.QUERY_EDITOR_SET_SCHEMA,
@@ -161,7 +163,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.schema).toBe(schema);
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
});
it('should not fail while setting autorun', () => {
test('should not fail while setting autorun', () => {
const action = {
type: actions.QUERY_EDITOR_SET_AUTORUN,
queryEditor: qe,
@@ -173,7 +175,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.autorun).toBe(true);
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 action = {
type: actions.QUERY_EDITOR_SET_TITLE,
@@ -184,7 +186,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.name).toBe(title);
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 action = {
type: actions.QUERY_EDITOR_SET_SQL,
@@ -195,7 +197,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.sql).toBe(sql);
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 action = {
type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
@@ -206,7 +208,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
expect(newState.unsavedQueryEditor.id).toBe(qe.id);
});
it('should set selectedText', () => {
test('should set selectedText', () => {
const selectedText = 'TEST';
const action = {
type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
@@ -218,7 +220,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
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 action = {
type: actions.QUERY_EDITOR_SET_SQL,
@@ -239,7 +241,7 @@ describe('sqlLabReducer', () => {
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 index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
const newQueryEditor = {
@@ -267,7 +269,7 @@ describe('sqlLabReducer', () => {
newQueryEditor.tabViewId,
);
});
it('should clear the destroyed query editors', () => {
test('should clear the destroyed query editors', () => {
const expectedQEId = '1233289';
const action = {
type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
@@ -285,6 +287,7 @@ describe('sqlLabReducer', () => {
expect(newState.destroyedQueryEditors).toEqual({});
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Tables', () => {
let newState;
let newTable;
@@ -297,12 +300,12 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(initialState, action);
newTable = newState.tables[0];
});
it('should add a table', () => {
test('should add a table', () => {
// Testing that beforeEach actually added the table
expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].expanded).toBe(true);
});
it('should merge the table attributes', () => {
test('should merge the table attributes', () => {
// Merging the extra attribute
newTable.extra = true;
const action = {
@@ -313,7 +316,7 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(1);
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 = {
type: actions.MERGE_TABLE,
table: newTable,
@@ -345,7 +348,7 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].id).toBe(remoteId);
});
it('should expand and collapse a table', () => {
test('should expand and collapse a table', () => {
const collapseTableAction = {
type: actions.COLLAPSE_TABLE,
table: newTable,
@@ -359,7 +362,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, expandTableAction);
expect(newState.tables[0].expanded).toBe(true);
});
it('should remove a table', () => {
test('should remove a table', () => {
const action = {
type: actions.REMOVE_TABLES,
tables: [newTable],
@@ -368,6 +371,7 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(0);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Run Query', () => {
const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439';
const CHANGED_ON_TIMESTAMP = 1687765985439;
@@ -385,7 +389,7 @@ describe('sqlLabReducer', () => {
sqlEditorId: 'dfsadfs',
};
});
it('should start a query', () => {
test('should start a query', () => {
const action = {
type: actions.START_QUERY,
query: {
@@ -401,7 +405,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, action);
expect(Object.keys(newState.queries)).toHaveLength(1);
});
it('should stop the query', () => {
test('should stop the query', () => {
const startQueryAction = {
type: actions.START_QUERY,
query,
@@ -415,7 +419,7 @@ describe('sqlLabReducer', () => {
const q = newState.queries[Object.keys(newState.queries)[0]];
expect(q.state).toBe('stopped');
});
it('should remove a query', () => {
test('should remove a query', () => {
const startQueryAction = {
type: actions.START_QUERY,
query,
@@ -428,7 +432,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, removeQueryAction);
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 endDttmInStr = '1693433503500.23132';
newState = sqlLabReducer(
@@ -449,7 +453,7 @@ describe('sqlLabReducer', () => {
expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr));
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 = {
...query,
extra: {
@@ -478,10 +482,11 @@ describe('sqlLabReducer', () => {
expect(newState.queries.abcd).toBe(query);
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({}));
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('CLEAR_INACTIVE_QUERIES', () => {
let newState;
let query;
@@ -496,7 +501,7 @@ describe('sqlLabReducer', () => {
cached: false,
};
});
it('updates queries that have already been completed', () => {
test('updates queries that have already been completed', () => {
newState = sqlLabReducer(
{
...newState,

View File

@@ -29,6 +29,7 @@ import {
} from 'src/SqlLab/constants';
import { queries, defaultQueryEditor } from '../fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('reduxStateToLocalStorageHelper', () => {
const queriesObj: Record<string, any> = {};
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
const oldQuery = queries[0];
const { id, startDttm } = oldQuery;
@@ -51,7 +52,7 @@ describe('reduxStateToLocalStorageHelper', () => {
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 = {
...queries[0],
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' }];
expect(Object.keys(queryEditors[0])).toContain('dummy');

View File

@@ -29,13 +29,14 @@ const emptyEditor = {
remoteId: null,
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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 title = newQueryTabName([], 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 unsavedEditors = [
{ ...emptyEditor, name: `${untitledQueryText} 1` },

View File

@@ -20,38 +20,40 @@
import { alterForComparison, formatValueHandler, getRowsFromDiffs } from '.';
import { RowType } from '../types';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('alterForComparison', () => {
it('returns null for undefined value', () => {
test('returns null for undefined value', () => {
expect(alterForComparison(undefined)).toBeNull();
});
it('returns null for null value', () => {
test('returns null for null value', () => {
expect(alterForComparison(null)).toBeNull();
});
it('returns null for empty string value', () => {
test('returns null for empty string value', () => {
expect(alterForComparison('')).toBeNull();
});
it('returns null for empty array value', () => {
test('returns null for empty array value', () => {
expect(alterForComparison([])).toBeNull();
});
it('returns null for empty object value', () => {
test('returns null for empty object value', () => {
expect(alterForComparison({})).toBeNull();
});
it('returns value for non-empty array', () => {
test('returns value for non-empty array', () => {
const value = [1, 2, 3];
expect(alterForComparison(value)).toEqual(value);
});
it('returns value for non-empty object', () => {
test('returns value for non-empty object', () => {
const value = { key: 'value' };
expect(alterForComparison(value)).toEqual(value);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('formatValueHandler', () => {
const controlsMap = {
b: { type: 'BoundsControl', label: 'Bounds' },
@@ -61,7 +63,7 @@ describe('formatValueHandler', () => {
other_control: { type: 'OtherControl', label: 'Other' },
};
it('handles undefined value', () => {
test('handles undefined value', () => {
const value = undefined;
const key = 'b';
@@ -74,7 +76,7 @@ describe('formatValueHandler', () => {
expect(formattedValue).toBe('N/A');
});
it('handles null value', () => {
test('handles null value', () => {
const value = null;
const key = 'b';
@@ -87,7 +89,7 @@ describe('formatValueHandler', () => {
expect(formattedValue).toBe('null');
});
it('returns "[]" for empty filters', () => {
test('returns "[]" for empty filters', () => {
const value: unknown[] = [];
const key = 'adhoc_filters';
@@ -100,7 +102,7 @@ describe('formatValueHandler', () => {
expect(formattedValue).toBe('[]');
});
it('formats filters with array values', () => {
test('formats filters with array values', () => {
const filters = [
{
clause: 'WHERE',
@@ -129,7 +131,7 @@ describe('formatValueHandler', () => {
expect(formattedValue).toBe(expected);
});
it('formats filters with string values', () => {
test('formats filters with string values', () => {
const filters = [
{
clause: 'WHERE',
@@ -158,7 +160,7 @@ describe('formatValueHandler', () => {
expect(formattedValue).toBe(expected);
});
it('formats "Min" and "Max" for BoundsControl', () => {
test('formats "Min" and "Max" for BoundsControl', () => {
const value: number[] = [1, 2];
const key = 'b';
@@ -167,7 +169,7 @@ describe('formatValueHandler', () => {
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 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 key = 'metrics';
@@ -187,7 +189,7 @@ describe('formatValueHandler', () => {
expect(result).toEqual('SUM(Sales), Metric2');
});
it('formats boolean values as string', () => {
test('formats boolean values as string', () => {
const value1 = true;
const value2 = false;
const key = 'b';
@@ -207,7 +209,7 @@ describe('formatValueHandler', () => {
expect(formattedValue2).toBe('false');
});
it('formats array values correctly', () => {
test('formats array values correctly', () => {
const value = [
{ label: 'Label1' },
{ label: 'Label2' },
@@ -229,7 +231,7 @@ describe('formatValueHandler', () => {
expect(result).toEqual(expected);
});
it('formats string values correctly', () => {
test('formats string values correctly', () => {
const value = 'test';
const key = 'other_control';
@@ -238,7 +240,7 @@ describe('formatValueHandler', () => {
expect(result).toEqual('test');
});
it('formats number values correctly', () => {
test('formats number values correctly', () => {
const value = 123;
const key = 'other_control';
@@ -247,7 +249,7 @@ describe('formatValueHandler', () => {
expect(result).toEqual(123);
});
it('formats object values correctly', () => {
test('formats object values correctly', () => {
const value = { 1: 2, alpha: 'bravo' };
const key = 'other_control';
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', () => {
it('returns formatted rows for diffs', () => {
test('returns formatted rows for diffs', () => {
const diffs = {
metric: { before: [{ label: 'old' }], after: [{ label: 'new' }] },
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 = {
unknown: { before: 'a', after: 'b' },
};

View File

@@ -18,7 +18,6 @@
*/
import {
forwardRef,
Key,
ReactNode,
RefObject,
useCallback,
@@ -43,18 +42,18 @@ import {
useTheme,
} from '@superset-ui/core';
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 { Dropdown } from '@superset-ui/core/components';
import { updateDataMask } from 'src/dataMask/actions';
import DrillByModal from 'src/components/Chart/DrillBy/DrillByModal';
import { useDatasetDrillInfo } from 'src/hooks/apiResources/datasets';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { DrillDetailMenuItems } from '../DrillDetail';
import { useDrillDetailMenuItems } from '../useDrillDetailMenuItems';
import { getMenuAdjustedY } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { DrillByMenuItems } from '../DrillBy/DrillByMenuItems';
import { DrillBySubmenu } from '../DrillBy/DrillBySubmenu';
import DrillDetailModal from '../DrillDetail/DrillDetailModal';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
export enum ContextMenuItem {
CrossFilter,
@@ -94,8 +93,8 @@ const ChartContextMenu = (
}: ChartContextMenuProps,
ref: RefObject<ChartContextMenuRef>,
) => {
const theme = useTheme();
const dispatch = useDispatch();
const theme = useTheme();
const { canDrillToDetail, canDrillBy, canDownload } = usePermissions();
const crossFiltersEnabled = useSelector<RootState, boolean>(
@@ -104,7 +103,6 @@ const ChartContextMenu = (
const dashboardId = useSelector<RootState, number>(
({ dashboardInfo }) => dashboardInfo.id,
);
const [openKeys, setOpenKeys] = useState<Key[]>([]);
const [modalFilters, setFilters] = useState<BinaryQueryObjectFilterClause[]>(
[],
@@ -160,7 +158,6 @@ const ChartContextMenu = (
const closeContextMenu = useCallback(() => {
setVisible(false);
setOpenKeys([]);
onClose();
}, [onClose]);
@@ -177,7 +174,7 @@ const ChartContextMenu = (
setShowDrillByModal(false);
}, []);
const menuItems: React.JSX.Element[] = [];
const menuItems: MenuItem[] = [];
const showDrillToDetail =
isFeatureEnabled(FeatureFlag.DrillToDetail) &&
@@ -264,6 +261,20 @@ const ChartContextMenu = (
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) {
const isCrossFilterDisabled =
!isCrossFilteringSupportedByChart ||
@@ -305,74 +316,65 @@ const ChartContextMenu = (
</>
);
}
menuItems.push(
<>
<Menu.Item
key="cross-filtering-menu-item"
disabled={isCrossFilterDisabled}
onClick={() => {
if (filters?.crossFilter) {
dispatch(updateDataMask(id, filters.crossFilter.dataMask));
}
}}
>
{filters?.crossFilter?.isCurrentValueSelected ? (
t('Remove cross-filter')
) : (
<div>
{t('Add cross-filter')}
<MenuItemTooltip
title={crossFilteringTooltipTitle}
color={!isCrossFilterDisabled ? theme.colorIcon : undefined}
/>
</div>
)}
</Menu.Item>
{itemsCount > 1 && <Menu.Divider />}
</>,
{
key: 'cross-filtering-menu-item',
label: filters?.crossFilter?.isCurrentValueSelected ? (
t('Remove cross-filter')
) : (
<span>
{t('Add cross-filter')}
<MenuItemTooltip
title={crossFilteringTooltipTitle}
color={!isCrossFilterDisabled ? theme.colorIcon : undefined}
/>
</span>
),
disabled: isCrossFilterDisabled,
onClick: () => {
if (filters?.crossFilter) {
dispatch(updateDataMask(id, filters.crossFilter.dataMask));
}
},
},
...(itemsCount > 1
? [{ key: 'divider-1', type: 'divider' as const }]
: []),
);
}
if (showDrillToDetail) {
menuItems.push(
<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 || {})}
/>,
);
menuItems.push(...drillDetailMenuItems);
}
if (showDrillBy) {
let submenuIndex = 0;
if (showCrossFilters) {
submenuIndex += 1;
if (menuItems.length > 0) {
menuItems.push({ key: 'divider-drill-by', type: 'divider' as const });
}
if (showDrillToDetail) {
submenuIndex += 2;
}
menuItems.push(
<DrillByMenuItems
drillByConfig={enhancedFilters?.drillBy}
onSelection={onSelection}
onCloseMenu={closeContextMenu}
formData={formData}
contextMenuY={clientY}
submenuIndex={submenuIndex}
open={openKeys.includes('drill-by-submenu')}
key="drill-by-submenu"
onDrillBy={handleDrillBy}
dataset={filteredDataset}
isLoadingDataset={isLoadingDataset}
{...(additionalConfig?.drillBy || {})}
/>,
);
const hasDrillBy = enhancedFilters?.drillBy?.groupbyFieldName;
const handlesDimensionContextMenu = getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DrillBy);
const isDrillByDisabled = !handlesDimensionContextMenu || !hasDrillBy;
// Add a custom render component for DrillBy submenu to support react-window
menuItems.push({
key: 'drill-by-submenu',
disabled: isDrillByDisabled,
label: (
<DrillBySubmenu
drillByConfig={enhancedFilters?.drillBy}
onSelection={onSelection}
onCloseMenu={closeContextMenu}
formData={formData}
onDrillBy={handleDrillBy}
dataset={filteredDataset}
isLoadingDataset={isLoadingDataset}
{...(additionalConfig?.drillBy || {})}
/>
),
});
}
const open = useCallback(
@@ -404,30 +406,22 @@ const ChartContextMenu = (
return ReactDOM.createPortal(
<>
<Dropdown
popupRender={() => (
<Menu
className="chart-context-menu"
data-test="chart-context-menu"
onOpenChange={setOpenKeys}
onClick={() => {
setVisible(false);
setOpenKeys([]);
onClose();
}}
>
{menuItems.length ? (
menuItems
) : (
<Menu.Item disabled>{t('No actions')}</Menu.Item>
)}
</Menu>
menu={{
items:
menuItems.length > 0
? menuItems
: [{ key: 'no-actions', label: t('No actions'), disabled: true }],
onClick: () => {
setVisible(false);
onClose();
},
}}
dropdownRender={menu => (
<div data-test="chart-context-menu">{menu}</div>
)}
trigger={['click']}
onOpenChange={value => {
setVisible(value);
if (!value) {
setOpenKeys([]);
}
}}
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', () => {
const defaultProps = {
chartId: 1,
@@ -49,7 +50,7 @@ describe('ChartErrorMessage', () => {
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({
result: null,
status: ResourceStatus.Loading,
@@ -61,7 +62,7 @@ describe('ChartErrorMessage', () => {
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(
'VALID_KEY',
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();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Embedded mode behavior', () => {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
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', () => {
beforeEach(() => {
// Mock a large dataset response for pagination testing

View File

@@ -30,9 +30,8 @@ import {
waitFor,
} from 'spec/helpers/testing-library';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { Menu } from '@superset-ui/core/components/Menu';
import { supersetGetCache } from 'src/utils/cachedSupersetGet';
import { DrillByMenuItems, DrillByMenuItemsProps } from './DrillByMenuItems';
import { DrillBySubmenu, DrillBySubmenuProps } from './DrillBySubmenu';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
@@ -79,37 +78,29 @@ const defaultFilters = [
},
];
const renderMenu = ({
const renderSubmenu = ({
formData = defaultFormData,
drillByConfig = { filters: defaultFilters, groupbyFieldName: 'groupby' },
dataset = mockDataset,
...rest
}: Partial<DrillByMenuItemsProps>) =>
}: Partial<DrillBySubmenuProps>) =>
render(
<Menu forceSubMenuRender>
<DrillByMenuItems
formData={formData ?? defaultFormData}
drillByConfig={drillByConfig}
dataset={dataset}
open
{...rest}
/>
</Menu>,
<DrillBySubmenu
formData={formData ?? defaultFormData}
drillByConfig={drillByConfig}
dataset={dataset}
{...rest}
/>,
{ useRouter: true, useRedux: true },
);
const expectDrillByDisabled = async (tooltipContent: string) => {
const drillByMenuItem = screen
.getAllByRole('menuitem')
.find(menuItem => within(menuItem).queryByText('Drill by'));
const drillByButton = screen.getByRole('button', { name: /drill by/i });
expect(drillByButton).toBeInTheDocument();
expect(drillByButton).toBeVisible();
expect(drillByButton).toHaveAttribute('tabindex', '-1');
expect(drillByMenuItem).toBeDefined();
expect(drillByMenuItem).toBeVisible();
expect(drillByMenuItem).toHaveAttribute('aria-disabled', 'true');
const tooltipTrigger = within(drillByMenuItem!).getByTestId(
'tooltip-trigger',
);
const tooltipTrigger = within(drillByButton).getByTestId('tooltip-trigger');
userEvent.hover(tooltipTrigger as HTMLElement);
const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
@@ -117,20 +108,17 @@ const expectDrillByDisabled = async (tooltipContent: string) => {
};
const expectDrillByEnabled = async () => {
const drillByMenuItem = screen.getByRole('menuitem', {
name: 'Drill by',
});
expect(drillByMenuItem).toBeInTheDocument();
await waitFor(() =>
expect(drillByMenuItem).not.toHaveAttribute('aria-disabled'),
);
const tooltipTrigger =
within(drillByMenuItem).queryByTestId('tooltip-trigger');
const drillByButton = screen.getByRole('button', { name: /drill by/i });
expect(drillByButton).toBeInTheDocument();
expect(drillByButton).not.toHaveAttribute('tabindex', '-1');
const tooltipTrigger = within(drillByButton).queryByTestId('tooltip-trigger');
expect(tooltipTrigger).not.toBeInTheDocument();
userEvent.hover(within(drillByMenuItem).getByText('Drill by'));
const drillBySubmenus = await screen.findAllByTestId('drill-by-submenu');
expect(drillBySubmenus[0]).toBeInTheDocument();
userEvent.hover(drillByButton);
const popover = await screen.findByRole('menu');
expect(popover).toBeInTheDocument();
};
getChartMetadataRegistry().registerValue(
@@ -149,7 +137,7 @@ afterEach(() => {
});
test('render disabled menu item for unsupported chart', async () => {
renderMenu({
renderSubmenu({
formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
});
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 () => {
renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } });
renderSubmenu({
drillByConfig: { filters: [], groupbyFieldName: 'groupby' },
});
await expectDrillByEnabled();
});
test('render disabled menu item for supported chart, no columns', async () => {
const emptyDataset = { ...mockDataset, columns: [], drillable_columns: [] };
renderMenu({ dataset: emptyDataset });
renderSubmenu({ dataset: emptyDataset });
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 () => {
const slicedColumns = defaultColumns.slice(0, 9);
const slicedColumns = defaultColumns.slice(0, 1); // Use only 1 column to avoid search box
const datasetWithSlicedColumns = {
...mockDataset,
columns: slicedColumns,
drillable_columns: slicedColumns,
};
renderMenu({ dataset: datasetWithSlicedColumns });
renderSubmenu({ dataset: datasetWithSlicedColumns });
await expectDrillByEnabled();
// Check that each column appears in the drill-by submenu
slicedColumns.forEach(column => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0]; // Use the first submenu
expect(within(submenu).getByText(column.column_name)).toBeInTheDocument();
});
// Check that the column appears in the popover
const col1Element = await screen.findByText('col1');
expect(col1Element).toBeInTheDocument();
// Should not have search box for small number of columns
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
// Add global timeout for all tests
jest.setTimeout(20000);
test('render menu item with submenu and searchbox', async () => {
renderMenu({ dataset: mockDataset });
renderSubmenu({ dataset: mockDataset });
await expectDrillByEnabled();
// Wait for all columns to be visible
await waitFor(
() => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
defaultColumns.forEach(column => {
expect(
within(submenu).getByText(column.column_name),
).toBeInTheDocument();
});
},
{ timeout: 10000 },
);
// Wait for first column to ensure menu is loaded
await screen.findByText('col1');
const searchbox = await waitFor(
() => screen.getAllByPlaceholderText('Search columns')[0],
);
// Then check all columns are visible
defaultColumns.forEach(column => {
expect(screen.getByText(column.column_name)).toBeInTheDocument();
});
const searchbox = screen.getByPlaceholderText('Search columns');
expect(searchbox).toBeInTheDocument();
userEvent.type(searchbox, 'col1');
const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
// Wait for filtered results
// Wait for filtering to take effect by checking for first filtered item
await waitFor(() => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
expectedFilteredColumnNames.forEach(colName => {
expect(within(submenu).getByText(colName)).toBeInTheDocument();
});
// Check that non-matching columns are not visible
expect(screen.queryByText('col2')).not.toBeInTheDocument();
});
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
// Then verify all expected columns are visible
expectedFilteredColumnNames.forEach(colName => {
expect(screen.getByText(colName)).toBeInTheDocument();
});
// Check that non-matching columns are not visible
defaultColumns
.filter(col => !expectedFilteredColumnNames.includes(col.column_name))
.forEach(col => {
expect(
within(submenu).queryByText(col.column_name),
).not.toBeInTheDocument();
expect(screen.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 () => {
@@ -252,7 +226,7 @@ test('Do not display excluded column in the menu', async () => {
...mockDataset,
drillable_columns: filteredColumns,
};
renderMenu({
renderSubmenu({
dataset: datasetWithFilteredColumns,
excludedColumns: excludedColNames.map(colName => ({
column_name: colName,
@@ -261,32 +235,24 @@ test('Do not display excluded column in the menu', async () => {
await expectDrillByEnabled();
// Wait for menu items to be loaded
await waitFor(
() => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
defaultColumns
.filter(column => !excludedColNames.includes(column.column_name))
.forEach(column => {
expect(
within(submenu).getByText(column.column_name),
).toBeInTheDocument();
});
},
{ timeout: 10000 },
);
// Wait for first column to ensure menu is loaded
await screen.findByText('col1');
// Then check all non-excluded columns are visible
defaultColumns
.filter(column => !excludedColNames.includes(column.column_name))
.forEach(column => {
expect(screen.getByText(column.column_name)).toBeInTheDocument();
});
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
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 () => {
const onSelectionMock = jest.fn();
renderMenu({
renderSubmenu({
dataset: mockDataset,
onSelection: onSelectionMock,
});
@@ -294,11 +260,7 @@ test('When menu item is clicked, call onSelection with clicked column and drill
await expectDrillByEnabled();
// Wait for col1 to be visible before clicking
const col1Element = await waitFor(() => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
return within(submenu).getByText('col1');
});
const col1Element = await screen.findByText('col1');
userEvent.click(col1Element);
expect(onSelectionMock).toHaveBeenCalledWith(
@@ -309,3 +271,10 @@ test('When menu item is clicked, call onSelection with clicked column and drill
{ 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(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('chart actions', () => {
const MOCK_URL = '/mockURL';
let dispatch;
@@ -121,12 +122,13 @@ describe('chart actions', () => {
};
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('v1 API', () => {
beforeEach(() => {
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);
await actionThunk(dispatch, mockGetState);
@@ -141,7 +143,7 @@ describe('chart actions', () => {
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();
const mockBigIntUrl = '/mock/chart/data/bigint';
const expectedBigNumber = '9223372036854775807';
@@ -160,7 +162,7 @@ describe('chart actions', () => {
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(
{ status: 200 },
{ result: [1, 2, 3] },
@@ -168,7 +170,7 @@ describe('chart actions', () => {
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 = {
[FeatureFlag.GlobalAsyncQueries]: true,
};
@@ -179,7 +181,7 @@ describe('chart actions', () => {
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 = {
[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', () => {
beforeEach(() => {
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({});
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({});
return actionThunk(dispatch, mockGetState).then(() => {
// 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({});
return actionThunk(dispatch, mockGetState).then(() => {
// 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({});
return actionThunk(dispatch, mockGetState).then(() => {
// 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({});
return actionThunk(dispatch, mockGetState).then(() => {
// 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(() => {});
fetchMock.post(MOCK_URL, () => unresolvingPromise, {
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(
MOCK_URL,
{ 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();
const mockBigIntUrl = '/mock/chart/data/bigint';
const expectedBigNumber = '9223372036854775807';
@@ -310,13 +313,14 @@ describe('chart actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runAnnotationQuery', () => {
const mockDispatch = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
test('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
const annotation = {
name: 'Holidays',
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', () => {
beforeEach(() => {
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');
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
const timeout = 10; // Set the timeout value here
@@ -403,7 +408,7 @@ describe('chart actions timeout', () => {
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');
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
const formData = { datasource: 'table__1' }; // Set the formData here

View File

@@ -19,6 +19,7 @@
import chartReducer, { chart } from 'src/components/Chart/chartReducer';
import * as actions from 'src/components/Chart/chartAction';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('chart reducers', () => {
const chartKey = 1;
let testChart;
@@ -31,13 +32,13 @@ describe('chart reducers', () => {
charts = { [chartKey]: testChart };
});
it('should update endtime on fail', () => {
test('should update endtime on fail', () => {
const newState = chartReducer(charts, actions.chartUpdateStopped(chartKey));
expect(newState[chartKey].chartUpdateEndTime).toBeGreaterThan(0);
expect(newState[chartKey].chartStatus).toEqual('stopped');
});
it('should update endtime on timeout', () => {
test('should update endtime on timeout', () => {
const newState = chartReducer(
charts,
actions.chartUpdateFailed(

View File

@@ -37,11 +37,12 @@ import {
t,
} from '@superset-ui/core';
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 { getSubmenuYOffset } from '../utils';
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_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 = ({
children,
stripHTML = false,
@@ -103,7 +82,7 @@ const StyledFilter = styled(Filter)`
`}
`;
export type DrillDetailMenuItemsArgs = {
export type DrillDetailMenuItemsProps = {
formData: QueryFormData;
filters?: BinaryQueryObjectFilterClause[];
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
@@ -115,6 +94,8 @@ export type DrillDetailMenuItemsArgs = {
setShowModal: (show: boolean) => void;
key?: string;
forceSubmenuRender?: boolean;
dataset?: Dataset;
isLoadingDataset?: boolean;
};
export const useDrillDetailMenuItems = ({
@@ -129,7 +110,7 @@ export const useDrillDetailMenuItems = ({
setShowModal,
key,
...props
}: DrillDetailMenuItemsArgs) => {
}: DrillDetailMenuItemsProps): ItemType[] => {
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
({ datasources }) =>
datasources[formData.datasource]?.database?.disable_drill_to_detail,
@@ -142,7 +123,7 @@ export const useDrillDetailMenuItems = ({
setFilters(filters);
setShowModal(true);
},
[onClick, onSelection],
[onClick, onSelection, setFilters, setShowModal],
);
// 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;
}
const drillToDetailMenuItem: MenuItem = drillDisabled
? getDisabledMenuItem(
<>
{DRILL_TO_DETAIL}
<MenuItemTooltip title={drillDisabled} />
</>,
'drill-to-detail-disabled',
props,
)
const drillToDetailMenuItem: ItemType = drillDisabled
? {
key: 'drill-to-detail-disabled',
disabled: true,
label: (
<div
css={css`
white-space: normal;
max-width: 160px;
`}
>
{DRILL_TO_DETAIL}
<MenuItemTooltip title={drillDisabled} />
</div>
),
...props,
}
: {
key: 'drill-to-detail',
label: DRILL_TO_DETAIL,
onClick: openModal.bind(null, []),
...props,
label: DRILL_TO_DETAIL,
};
const getMenuItemWithTruncation = useMenuItemWithTruncation();
const drillToDetailByMenuItem: MenuItem = drillByDisabled
? getDisabledMenuItem(
<>
{DRILL_TO_DETAIL_BY}
<MenuItemTooltip title={drillByDisabled} />
</>,
'drill-to-detail-by-disabled',
props,
)
: {
key: key || 'drill-to-detail-by',
label: DRILL_TO_DETAIL_BY,
children: [
...filters.map((filter, i) => ({
key: `drill-detail-filter-${i}`,
label: getMenuItemWithTruncation({
tooltipText: `${DRILL_TO_DETAIL_BY} ${filter.formattedVal}`,
onClick: openModal.bind(null, [filter]),
const drillToDetailByMenuItem: ItemType | null = !isContextMenu
? null
: drillByDisabled
? {
key: 'drill-to-detail-by-disabled',
disabled: true,
label: (
<div
css={css`
white-space: normal;
max-width: 160px;
`}
>
{DRILL_TO_DETAIL_BY}
<MenuItemTooltip title={drillByDisabled} />
</div>
),
...props,
}
: {
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}`,
children: (
<>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML>{filter.formattedVal}</StyledFilter>
</>
onClick: openModal.bind(null, [filter]),
label: (
<div
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 && {
key: 'drill-detail-filter-all',
label: getMenuItemWithTruncation({
tooltipText: `${DRILL_TO_DETAIL_BY} ${t('all')}`,
onClick: openModal.bind(null, filters),
key: 'drill-detail-filter-all',
children: (
<>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML={false}>{t('all')}</StyledFilter>
</>
),
}),
},
].filter(Boolean) as MenuItem[],
onClick: openModal.bind(null, filters),
forceSubmenuRender: true,
popupOffset: [0, submenuYOffset],
popupClassName: 'chart-context-submenu',
...props,
};
if (isContextMenu) {
return {
drillToDetailMenuItem,
drillToDetailByMenuItem,
};
})),
...(filters.length > 1
? [
{
key: 'drill-detail-filter-all',
onClick: openModal.bind(null, filters),
label: (
<div
aria-label={`${DRILL_TO_DETAIL_BY} ${t('all')}`}
css={css`
max-width: 200px;
`}
>
{`${DRILL_TO_DETAIL_BY} `}
<StyledFilter stripHTML={false}>
{t('all')}
</StyledFilter>
</div>
),
},
]
: []),
],
...props,
};
const menuItems: ItemType[] = [drillToDetailMenuItem];
if (drillToDetailByMenuItem) {
menuItems.push(drillToDetailByMenuItem);
}
return {
drillToDetailMenuItem,
};
return menuItems;
};

View File

@@ -22,6 +22,7 @@ import {
render,
screen,
userEvent,
waitFor,
within,
} from 'spec/helpers/testing-library';
import setupPlugins from 'src/setup/setupPlugins';
@@ -29,15 +30,13 @@ import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { BinaryQueryObjectFilterClause, VizType } from '@superset-ui/core';
import { Menu } from '@superset-ui/core/components/Menu';
import DrillDetailMenuItems, {
DrillDetailMenuItemsProps,
} from './DrillDetailMenuItems';
import DrillDetailModal from './DrillDetailModal';
import DrillDetailModal from '../DrillDetail/DrillDetailModal';
import { useDrillDetailMenuItems, DrillDetailMenuItemsProps } from './index';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
jest.mock(
'./DrillDetailPane',
'../DrillDetail/DrillDetailPane',
() =>
({
initialFilters,
@@ -87,17 +86,17 @@ const MockRenderChart = ({
BinaryQueryObjectFilterClause[] | undefined
>(filters);
const menuItems = useDrillDetailMenuItems({
setFilters,
formData: formData ?? defaultFormData,
filters: modalFilters,
isContextMenu,
setShowModal: setShowMenu,
});
return (
<>
<Menu forceSubMenuRender>
<DrillDetailMenuItems
setFilters={setFilters}
formData={formData ?? defaultFormData}
filters={modalFilters}
isContextMenu={isContextMenu}
setShowModal={setShowMenu}
/>
</Menu>
<Menu forceSubMenuRender items={menuItems} />
<DrillDetailModal
chartId={chartId ?? defaultChartId}
@@ -130,8 +129,10 @@ const renderMenu = ({
);
};
const setupMenu = (filters: BinaryQueryObjectFilterClause[]) => {
const setupMenu = async (filters: BinaryQueryObjectFilterClause[]) => {
cleanup();
// Small delay to ensure DOM cleanup is complete
await new Promise(resolve => setTimeout(resolve, 10));
renderMenu({
chartId: defaultChartId,
formData: defaultFormData,
@@ -235,11 +236,11 @@ const expectDrillToDetailByEnabled = async () => {
.getAllByRole('menuitem')
.find(menuItem => within(menuItem).queryByText('Drill to detail by'));
await expectMenuItemEnabled(drillToDetailBy!);
userEvent.hover(drillToDetailBy!);
const submenus = await screen.findAllByTestId('drill-to-detail-by-submenu');
expect(submenus.length).toEqual(2);
const submenu = await screen.findByRole('menu', {});
expect(submenu).toBeInTheDocument();
};
/**
@@ -258,17 +259,37 @@ const expectDrillToDetailByDisabled = async (tooltipContent?: string) => {
const expectDrillToDetailByDimension = async (
filter: BinaryQueryObjectFilterClause,
) => {
userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' }));
const drillToDetailBySubMenus = await screen.findAllByTestId(
'drill-to-detail-by-submenu',
);
const formattedVal = filter.formattedVal as string;
const drillByMenuItem = screen.getByRole('menuitem', {
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 drillToDetailBySubmenuItems = await within(
drillToDetailBySubMenus[1],
).findAllByRole('menuitem');
await expectMenuItemEnabled(drillToDetailBySubmenuItems[0]);
await expectMenuItemEnabled(drillToDetailBySubmenuItem);
await expectDrillToDetailModal(menuItemName, [filter]);
};
@@ -278,14 +299,15 @@ const expectDrillToDetailByDimension = async (
const expectDrillToDetailByAll = async (
filters: BinaryQueryObjectFilterClause[],
) => {
userEvent.hover(screen.getByRole('menuitem', { name: 'Drill to detail by' }));
const drillToDetailBySubMenus = await screen.findAllByTestId(
'drill-to-detail-by-submenu',
);
const drillByMenuItem = screen.getByRole('menuitem', {
name: 'Drill to detail by',
});
const drillToDetailBySubmenuItem = await within(
drillToDetailBySubMenus[1],
).findByText(/all/i);
userEvent.hover(drillByMenuItem);
await screen.findByRole('menu');
const drillToDetailBySubmenuItem = await screen.findByText(/all/i, {});
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 () => {
const filters = [filterA, filterB];
setupMenu(filters);
await setupMenu(filters);
await expectDrillToDetailByEnabled();
await expectDrillToDetailByDimension(filterA);
});
test('context menu for supported chart, dimensions, filter B', async () => {
const filters = [filterA, filterB];
setupMenu(filters);
await setupMenu(filters);
await expectDrillToDetailByEnabled();
await expectDrillToDetailByDimension(filterB);
});
test.skip('context menu for supported chart, dimensions, all filters', async () => {
const filters = [filterA, filterB];
setupMenu(filters);
await setupMenu(filters);
await expectDrillToDetailByAll(filters);
});

View File

@@ -73,24 +73,25 @@ beforeEach(() => {
fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] });
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceModal', () => {
it('renders', async () => {
test('renders', async () => {
expect(container).toBeDefined();
});
it('renders the component', () => {
test('renders the component', () => {
expect(screen.getByText('Edit Dataset')).toBeInTheDocument();
});
it('renders a Modal', async () => {
test('renders a Modal', async () => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
it('renders a DatasourceEditor', async () => {
test('renders a DatasourceEditor', async () => {
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
// 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
@@ -104,7 +105,7 @@ describe('DatasourceModal', () => {
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();
const onDatasourceSave = jest.fn();
@@ -120,7 +121,7 @@ describe('DatasourceModal', () => {
});
});
it('should render error dialog', async () => {
test('should render error dialog', async () => {
jest
.spyOn(SupersetClient, 'put')
.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
* under the License.
*/
import {
FunctionComponent,
useState,
useRef,
useEffect,
useCallback,
} from 'react';
import { FunctionComponent, useState, useEffect, useCallback } from 'react';
import { useSelector } from 'react-redux';
import {
styled,
@@ -101,8 +95,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
}) => {
const theme = useTheme();
const [currentDatasource, setCurrentDatasource] = useState(datasource);
const syncColumnsRef = useRef(false);
const [confirmModal, setConfirmModal] = useState<any>(null);
const [syncColumns, setSyncColumns] = useState(false);
const currencies = useSelector<
{
common: {
@@ -114,7 +107,6 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
const [errors, setErrors] = useState<any[]>([]);
const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
const dialog = useRef<any>(null);
const [modal, contextHolder] = Modal.useModal();
const buildPayload = (datasource: Record<string, any>) => {
const payload: Record<string, any> = {
@@ -196,7 +188,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
setIsSaving(true);
try {
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),
});
@@ -281,14 +273,9 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
impact the column definitions, you might want to skip this step.`)}
/>
<Checkbox
checked={syncColumnsRef.current}
checked={syncColumns}
onChange={() => {
syncColumnsRef.current = !syncColumnsRef.current;
if (confirmModal) {
confirmModal.update({
content: getSaveDialog(),
});
}
setSyncColumns(!syncColumns);
}}
/>
<span
@@ -303,25 +290,17 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
{t('Are you sure you want to save and apply changes?')}
</div>
),
[currentDatasource.sql, datasource.sql, confirmModal],
[currentDatasource.sql, datasource.sql, syncColumns],
);
useEffect(() => {
if (confirmModal) {
confirmModal.update({
content: getSaveDialog(),
});
}
}, [confirmModal, getSaveDialog]);
useEffect(() => {
if (datasource.sql !== currentDatasource.sql) {
syncColumnsRef.current = true;
setSyncColumns(true);
}
}, [datasource.sql, currentDatasource.sql]);
const onClickSave = () => {
const modalInstance = modal.confirm({
modal.confirm({
title: t('Confirm save'),
content: getSaveDialog(),
onOk: onConfirmSave,
@@ -329,8 +308,6 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
okText: t('OK'),
cancelText: t('Cancel'),
});
setConfirmModal(modalInstance);
dialog.current = modalInstance;
};
return (

View File

@@ -104,10 +104,10 @@ export default class CRUDCollection extends PureComponent<
this.toggleExpand = this.toggleExpand.bind(this);
}
UNSAFE_componentWillReceiveProps(nextProps: CRUDCollectionProps) {
if (nextProps.collection !== this.props.collection) {
componentDidUpdate(prevProps: CRUDCollectionProps) {
if (this.props.collection !== prevProps.collection) {
const { collection, collectionArray } = createKeyedCollection(
nextProps.collection,
this.props.collection,
);
this.setState(prevState => ({
collection,

View File

@@ -740,7 +740,6 @@ class DatasourceEditor extends PureComponent {
this.props.runQuery({
client_id: this.props.clientId,
database_id: this.state.datasource.database.id,
json: true,
runAsync: false,
catalog: this.state.datasource.catalog,
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', () => {
test('renders empty state when no dashboards provided', () => {
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', () => {
beforeAll(() => {
jest.clearAllMocks();
@@ -80,11 +81,11 @@ describe('DatasourceEditor', () => {
// jest.clearAllMocks();
});
it('renders Tabs', () => {
test('renders Tabs', () => {
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');
userEvent.click(columnsTab);
@@ -111,7 +112,7 @@ describe('DatasourceEditor', () => {
});
// to add, remove and modify columns accordingly
it('can modify columns', async () => {
test('can modify columns', async () => {
const columnsTab = screen.getByTestId('collection-tab-Columns');
userEvent.click(columnsTab);
@@ -138,7 +139,7 @@ describe('DatasourceEditor', () => {
userEvent.type(inputCertDetails, 'test');
}, 40000);
it('can delete columns', async () => {
test('can delete columns', async () => {
const columnsTab = screen.getByTestId('collection-tab-Columns');
userEvent.click(columnsTab);
@@ -162,7 +163,7 @@ describe('DatasourceEditor', () => {
});
}, 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');
userEvent.click(calcColsTab);
@@ -180,7 +181,7 @@ describe('DatasourceEditor', () => {
});
}, 60000);
it('renders isSqla fields', async () => {
test('renders isSqla fields', async () => {
const columnsTab = screen.getByRole('tab', {
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', () => {
beforeAll(() => {
isFeatureEnabled.mockImplementation(() => false);
@@ -216,7 +218,7 @@ describe('DatasourceEditor Source Tab', () => {
isFeatureEnabled.mockRestore();
});
it('Source Tab: edit mode', async () => {
test('Source Tab: edit mode', async () => {
const getLockBtn = screen.getByRole('img', { name: /lock/i });
userEvent.click(getLockBtn);
@@ -231,7 +233,7 @@ describe('DatasourceEditor Source Tab', () => {
expect(virtualRadioBtn).toBeEnabled();
});
it('Source Tab: readOnly mode', () => {
test('Source Tab: readOnly mode', () => {
const getLockBtn = screen.getByRole('img', { name: /lock/i });
expect(getLockBtn).toBeInTheDocument();
@@ -246,7 +248,7 @@ describe('DatasourceEditor Source Tab', () => {
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
cleanup();

View File

@@ -30,6 +30,7 @@ const fastRender = props =>
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceEditor Currency Tests', () => {
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
@@ -40,7 +41,7 @@ describe('DatasourceEditor Currency Tests', () => {
});
// The problematic test, now optimized
it('renders currency controls', async () => {
test('renders currency controls', async () => {
// Setup a metric with currency data
const propsWithCurrency = {
...props,

View File

@@ -24,6 +24,7 @@ import {
DATASOURCE_ENDPOINT,
} from './DatasourceEditor.test';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceEditor RTL Metrics Tests', () => {
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
@@ -34,7 +35,7 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
fetchMock.restore();
});
it('properly renders the metric information', async () => {
test('properly renders the metric information', async () => {
await asyncRender(props);
const metricButton = screen.getByTestId('collection-tab-Metrics');
@@ -52,7 +53,7 @@ describe('DatasourceEditor RTL Metrics Tests', () => {
expect(warningMarkdown.value).toEqual('someone');
});
it('properly updates the metric information', async () => {
test('properly updates the metric information', async () => {
await asyncRender(props);
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', () => {
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { overwriteRoutes: true });
@@ -92,7 +94,7 @@ describe('DatasourceEditor RTL Columns Tests', () => {
fetchMock.restore();
});
it('shows the default datetime column', async () => {
test('shows the default datetime column', async () => {
await asyncRender(props);
const columnsButton = screen.getByTestId('collection-tab-Columns');
@@ -107,7 +109,7 @@ describe('DatasourceEditor RTL Columns Tests', () => {
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);
const columnsButton = screen.getByTestId('collection-tab-Columns');

View File

@@ -20,6 +20,7 @@
import { tn } from '@superset-ui/core';
import { updateColumns } from '.';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('updateColumns', () => {
let addSuccessToast: jest.Mock;
@@ -27,7 +28,7 @@ describe('updateColumns', () => {
addSuccessToast = jest.fn();
});
it('adds new columns when prevCols is empty', () => {
test('adds new columns when prevCols is empty', () => {
interface Column {
column_name: 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 = [
{ column_name: 'col1', type: 'string', 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 = [
{ column_name: 'col1', type: 'string', is_dttm: false },
{ 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 = [
{ column_name: 'col1', type: 'string', 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 = [
{ column_name: 'col1', type: 'string', 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 { ErrorAlert } from './ErrorAlert';
describe('ErrorAlert', () => {
it('renders the error message correctly', () => {
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
/>,
);
// ErrorAlert
test('ErrorAlert renders the error message correctly', () => {
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
/>,
);
expect(screen.getByText('Error')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('renders the description when provided', () => {
const description = 'This is a detailed description';
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
description={description}
/>,
);
expect(screen.getByText(description)).toBeInTheDocument();
});
it('toggles description details visibility when show more/less is clicked', () => {
const descriptionDetails = 'Additional details about the error.';
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
descriptionDetails={descriptionDetails}
descriptionDetailsCollapsed
/>,
);
const showMoreButton = screen.getByText('See more');
expect(showMoreButton).toBeInTheDocument();
fireEvent.click(showMoreButton);
expect(screen.getByText(descriptionDetails)).toBeInTheDocument();
const showLessButton = screen.getByText('See less');
fireEvent.click(showLessButton);
expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument();
});
it('renders compact mode with a tooltip and modal', () => {
render(
<ErrorAlert
errorType="Error"
message="Compact mode example"
type="error"
compact
descriptionDetails="Detailed description in compact mode."
/>,
);
const iconTrigger = screen.getByText('Error');
expect(iconTrigger).toBeInTheDocument();
fireEvent.click(iconTrigger);
expect(screen.getByText('Compact mode example')).toBeInTheDocument();
});
expect(screen.getByText('Error')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
test('ErrorAlert renders the description when provided', () => {
const description = 'This is a detailed description';
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
description={description}
/>,
);
expect(screen.getByText(description)).toBeInTheDocument();
});
test('ErrorAlert toggles description details visibility when show more/less is clicked', () => {
const descriptionDetails = 'Additional details about the error.';
render(
<ErrorAlert
errorType="Error"
message="Something went wrong"
type="error"
descriptionDetails={descriptionDetails}
descriptionDetailsCollapsed
/>,
);
const showMoreButton = screen.getByText('See more');
expect(showMoreButton).toBeInTheDocument();
fireEvent.click(showMoreButton);
expect(screen.getByText(descriptionDetails)).toBeInTheDocument();
const showLessButton = screen.getByText('See less');
fireEvent.click(showLessButton);
expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument();
});
test('ErrorAlert renders compact mode with a tooltip and modal', () => {
render(
<ErrorAlert
errorType="Error"
message="Compact mode example"
type="error"
compact
descriptionDetails="Detailed description in compact mode."
/>,
);
const iconTrigger = screen.getByText('Error');
expect(iconTrigger).toBeInTheDocument();
fireEvent.click(iconTrigger);
expect(screen.getByText('Compact mode example')).toBeInTheDocument();
});

View File

@@ -54,6 +54,7 @@ const missingExtraProps = {
const renderComponent = (overrides = {}) =>
render(<InvalidSQLErrorMessage {...defaultProps} {...overrides} />);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('InvalidSQLErrorMessage', () => {
beforeAll(() => {
jest.setTimeout(30000);
@@ -64,7 +65,7 @@ describe('InvalidSQLErrorMessage', () => {
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();
// Validate main properties
@@ -75,13 +76,13 @@ describe('InvalidSQLErrorMessage', () => {
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);
expect(getByText('Unable to parse SQL')).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();
// Validate SQL and caret indicator
@@ -95,7 +96,7 @@ describe('InvalidSQLErrorMessage', () => {
unmount();
});
it('handles missing line number gracefully', async () => {
test('handles missing line number gracefully', async () => {
const overrides = {
error: {
...defaultProps.error,
@@ -114,7 +115,7 @@ describe('InvalidSQLErrorMessage', () => {
unmount();
});
it('handles missing column number gracefully', async () => {
test('handles missing column number gracefully', async () => {
const overrides = {
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 { MarshmallowErrorMessage } from './MarshmallowErrorMessage';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('MarshmallowErrorMessage', () => {
const mockError = {
extra: {

View File

@@ -99,15 +99,16 @@ const setup = (overrides = {}) => (
</Provider>
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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());
expect(getByText(/Authorization needed/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 linkElement = getByText(/provide authorization/i);
@@ -116,7 +117,7 @@ describe('OAuth2RedirectMessage Component', () => {
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());
expect(mockAddEventListener).toHaveBeenCalled();
@@ -124,7 +125,7 @@ describe('OAuth2RedirectMessage Component', () => {
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());
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' }));
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' }));
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');

View File

@@ -57,15 +57,16 @@ afterEach(() => {
cleanup();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FacePile', () => {
it('renders empty state with no users', () => {
test('renders empty state with no users', () => {
const { container } = render(<FacePile users={[]} />, { store });
expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument();
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)} />, {
store,
});
@@ -76,7 +77,7 @@ describe('FacePile', () => {
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)} />, {
store,
});
@@ -90,7 +91,7 @@ describe('FacePile', () => {
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 });
// Should show 4 avatars + 1 overflow indicator = 5 total elements
@@ -107,7 +108,7 @@ describe('FacePile', () => {
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)} />, {
store,
});
@@ -119,7 +120,7 @@ describe('FacePile', () => {
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
mockIsFeatureEnabled.mockImplementation(
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', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getRandomColor', () => {
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';
expect(getRandomColor(name, colors)).toEqual(
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(
getRandomColor('bar', colors),
);
});
it('handles non-ascii input values', () => {
test('handles non-ascii input values', () => {
expect(getRandomColor('泰', colors)).toMatchInlineSnapshot(`"color1"`);
expect(getRandomColor('مُحَمَّد‎', colors)).toMatchInlineSnapshot(
`"color2"`,

View File

@@ -26,6 +26,7 @@ import {
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import { FilterableTable } from '.';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FilterableTable', () => {
beforeAll(() => {
setupAGGridModules();
@@ -40,10 +41,10 @@ describe('FilterableTable', () => {
],
height: 500,
};
it('is valid element', () => {
test('is valid element', () => {
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(
<FilterableTable {...mockedProps} />,
);
@@ -52,7 +53,7 @@ describe('FilterableTable', () => {
expect(getByText(columnBContent)).toBeInTheDocument();
});
});
it('filters on a string', () => {
test('filters on a string', () => {
const props = {
...mockedProps,
filterText: 'b1',
@@ -62,7 +63,7 @@ describe('FilterableTable', () => {
expect(queryByText('b2')).not.toBeInTheDocument();
expect(queryByText('b3')).not.toBeInTheDocument();
});
it('filters on a number', () => {
test('filters on a number', () => {
const props = {
...mockedProps,
filterText: '100',
@@ -74,12 +75,13 @@ describe('FilterableTable', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FilterableTable sorting - RTL', () => {
beforeAll(() => {
setupAGGridModules();
});
it('sorts strings correctly', () => {
test('sorts strings correctly', () => {
const stringProps = {
orderedColumnKeys: ['columnA'],
data: [
@@ -128,7 +130,7 @@ describe('FilterableTable sorting - RTL', () => {
);
});
it('sorts integers correctly', () => {
test('sorts integers correctly', () => {
const integerProps = {
orderedColumnKeys: ['columnB'],
data: [{ columnB: 21 }, { columnB: 0 }, { columnB: 623 }],
@@ -163,7 +165,7 @@ describe('FilterableTable sorting - RTL', () => {
expect(gridCells?.textContent).toEqual(['21', '0', '623'].join(''));
});
it('sorts floating numbers correctly', () => {
test('sorts floating numbers correctly', () => {
const floatProps = {
orderedColumnKeys: ['columnC'],
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 = {
orderedColumnKeys: ['columnD'],
data: [
@@ -308,7 +310,7 @@ describe('FilterableTable sorting - RTL', () => {
);
});
it('sorts YYYY-MM-DD properly', () => {
test('sorts YYYY-MM-DD properly', () => {
const dsProps = {
orderedColumnKeys: ['columnDS'],
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);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('for main menu', () => {
test('renders Copy to Clipboard', async () => {
const { getByText } = setup({ ...mockedProps, isMain: true });

View File

@@ -130,6 +130,7 @@ const factory = (props = mockedProps) =>
{ store: mockStore() },
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ListView', () => {
beforeEach(() => {
fetchMock.reset();
@@ -146,7 +147,7 @@ describe('ListView', () => {
});
// Example of converted test:
it('calls fetchData on mount', () => {
test('calls fetchData on mount', () => {
expect(mockedProps.fetchData).toHaveBeenCalledWith({
filters: [],
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];
await userEvent.click(sortHeader);
@@ -173,7 +174,7 @@ describe('ListView', () => {
});
// Update pagination control tests for Ant Design pagination
it('renders pagination controls', () => {
test('renders pagination controls', () => {
const paginationList = screen.getByRole('list');
expect(paginationList).toBeInTheDocument();
@@ -181,7 +182,7 @@ describe('ListView', () => {
expect(pageOneItem).toBeInTheDocument();
});
it('calls fetchData on page change', async () => {
test('calls fetchData on page change', async () => {
const pageTwoItem = screen.getByRole('listitem', { name: '2' });
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');
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
it('renders UI filters', () => {
test('renders UI filters', () => {
const filterControls = screen.getAllByRole('combobox');
expect(filterControls).toHaveLength(2);
});
it('calls fetchData on filter', async () => {
test('calls fetchData on filter', async () => {
// Handle select filter
const selectFilter = screen.getAllByRole('combobox')[0];
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({
...mockedProps,
renderCard: jest.fn(),

View File

@@ -19,26 +19,25 @@
import { ADD_TOAST, REMOVE_TOAST } from 'src/components/MessageToasts/actions';
import messageToastsReducer from 'src/components/MessageToasts/reducers';
describe('messageToasts reducer', () => {
it('should return initial state', () => {
expect(messageToastsReducer(undefined, {})).toEqual([]);
});
it('should add a toast', () => {
expect(
messageToastsReducer([], {
type: ADD_TOAST,
payload: { text: 'test', id: 'id', type: 'test_type' },
}),
).toEqual([{ text: 'test', id: 'id', type: 'test_type' }]);
});
it('should remove a toast', () => {
expect(
messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
type: REMOVE_TOAST,
payload: { id: 'id' },
}),
).toEqual([{ id: 'id2' }]);
});
// messageToasts reducer
test('messageToasts reducer should return initial state', () => {
expect(messageToastsReducer(undefined, {})).toEqual([]);
});
test('messageToasts reducer should add a toast', () => {
expect(
messageToastsReducer([], {
type: ADD_TOAST,
payload: { text: 'test', id: 'id', type: 'test_type' },
}),
).toEqual([{ text: 'test', id: 'id', type: 'test_type' }]);
});
test('messageToasts reducer should remove a toast', () => {
expect(
messageToastsReducer([{ id: 'id' }, { id: 'id2' }], {
type: REMOVE_TOAST,
payload: { id: 'id' },
}),
).toEqual([{ id: 'id2' }]);
});

View File

@@ -18,7 +18,7 @@
*/
import { ReactNode } from 'react';
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';
interface StandardModalProps {
@@ -39,6 +39,7 @@ interface StandardModalProps {
destroyOnClose?: boolean;
maskClosable?: boolean;
wrapProps?: object;
contentLoading?: boolean;
}
// Standard modal widths
@@ -113,12 +114,13 @@ export function StandardModal({
destroyOnClose = true,
maskClosable = false,
wrapProps,
contentLoading = false,
}: StandardModalProps) {
const primaryButtonName = saveText || (isEditMode ? t('Save') : t('Add'));
return (
<StyledModal
disablePrimaryButton={saveDisabled || saveLoading}
disablePrimaryButton={saveDisabled || saveLoading || contentLoading}
primaryButtonLoading={saveLoading}
primaryTooltipMessage={errorTooltip}
onHandledPrimaryAction={onSave}
@@ -139,7 +141,13 @@ export function StandardModal({
)
}
>
{children}
{contentLoading ? (
<Flex justify="center" align="center" style={{ minHeight: 200 }}>
<Loading />
</Flex>
) : (
children
)}
</StyledModal>
);
}

View File

@@ -20,41 +20,40 @@ import { render, screen } from 'spec/helpers/testing-library';
import { Icons } from '@superset-ui/core/components';
import { ModalTitleWithIcon } from '.';
describe('ModalTitleWithIcon', () => {
it('renders the title without icon if none is passed and isEditMode is undefined', () => {
render(<ModalTitleWithIcon title="My Title" />);
expect(screen.getByText('My Title')).toBeInTheDocument();
// ModalTitleWithIcon
test('ModalTitleWithIcon renders the title without icon if none is passed and isEditMode is undefined', () => {
render(<ModalTitleWithIcon title="My Title" />);
expect(screen.getByText('My Title')).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders Edit icon if isEditMode is true', () => {
render(<ModalTitleWithIcon title="Edit Mode" isEditMode />);
expect(screen.getByText('Edit Mode')).toBeInTheDocument();
expect(screen.getByRole('img', { name: /edit/i })).toBeInTheDocument();
});
it('renders Plus icon if isEditMode is false', () => {
render(<ModalTitleWithIcon title="Add Mode" isEditMode={false} />);
expect(screen.getByText('Add Mode')).toBeInTheDocument();
expect(screen.getByRole('img', { name: /plus/i })).toBeInTheDocument();
});
it('renders custom icon when passed explicitly', () => {
render(
<ModalTitleWithIcon
title="Custom Icon"
icon={<Icons.DownOutlined data-test="custom-icon" />}
/>,
);
expect(screen.getByText('Custom Icon')).toBeInTheDocument();
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
});
it('respects the level prop (e.g., renders h3 for level=3)', () => {
const { container } = render(
<ModalTitleWithIcon title="Header Level 3" level={3} />,
);
expect(container.querySelector('h3')).toHaveTextContent('Header Level 3');
});
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
test('ModalTitleWithIcon renders Edit icon if isEditMode is true', () => {
render(<ModalTitleWithIcon title="Edit Mode" isEditMode />);
expect(screen.getByText('Edit Mode')).toBeInTheDocument();
expect(screen.getByRole('img', { name: /edit/i })).toBeInTheDocument();
});
test('ModalTitleWithIcon renders Plus icon if isEditMode is false', () => {
render(<ModalTitleWithIcon title="Add Mode" isEditMode={false} />);
expect(screen.getByText('Add Mode')).toBeInTheDocument();
expect(screen.getByRole('img', { name: /plus/i })).toBeInTheDocument();
});
test('ModalTitleWithIcon renders custom icon when passed explicitly', () => {
render(
<ModalTitleWithIcon
title="Custom Icon"
icon={<Icons.DownOutlined data-test="custom-icon" />}
/>,
);
expect(screen.getByText('Custom Icon')).toBeInTheDocument();
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
});
test('ModalTitleWithIcon respects the level prop (e.g., renders h3 for level=3)', () => {
const { container } = render(
<ModalTitleWithIcon title="Header Level 3" level={3} />,
);
expect(container.querySelector('h3')).toHaveTextContent('Header Level 3');
});

View File

@@ -26,6 +26,7 @@ import useStoredSidebarWidth from './useStoredSidebarWidth';
const INITIAL_WIDTH = 300;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('useStoredSidebarWidth', () => {
beforeEach(() => {
localStorage.clear();
@@ -35,7 +36,7 @@ describe('useStoredSidebarWidth', () => {
localStorage.clear();
});
it('returns a default filterBar width by initialWidth', () => {
test('returns a default filterBar width by initialWidth', () => {
const id = '123';
const { result } = renderHook(() =>
useStoredSidebarWidth(id, INITIAL_WIDTH),
@@ -45,7 +46,7 @@ describe('useStoredSidebarWidth', () => {
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 expectedWidth = 378;
setItem(LocalStorageKeys.CommonResizableSidebarWidths, {
@@ -61,7 +62,7 @@ describe('useStoredSidebarWidth', () => {
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 expectedWidth = 378;
const otherDashboardId = '456';

View File

@@ -41,12 +41,13 @@ const defaultProps = {
datasourceType: 'table',
};
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SQLEditorWithValidation', () => {
beforeEach(() => {
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} />);
expect(screen.getByText('Unverified')).toBeInTheDocument();
@@ -55,7 +56,7 @@ describe('SQLEditorWithValidation', () => {
).toBeInTheDocument();
});
it('does not render validation bar when showValidation is false', () => {
test('does not render validation bar when showValidation is false', () => {
render(
<SQLEditorWithValidation {...defaultProps} showValidation={false} />,
);
@@ -66,7 +67,7 @@ describe('SQLEditorWithValidation', () => {
).not.toBeInTheDocument();
});
it('shows primary button style when unverified', () => {
test('shows primary button style when unverified', () => {
render(<SQLEditorWithValidation {...defaultProps} />);
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)
});
it('disables validate button when no value or datasourceId', () => {
test('disables validate button when no value or datasourceId', () => {
render(
<SQLEditorWithValidation
{...defaultProps}
@@ -91,7 +92,7 @@ describe('SQLEditorWithValidation', () => {
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<
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<
typeof SupersetClient.post
>;
@@ -138,7 +139,7 @@ describe('SQLEditorWithValidation', () => {
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<
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<
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<
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<
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<
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} />);
// Simulate having a validation result
@@ -304,7 +305,7 @@ describe('SQLEditorWithValidation', () => {
expect(screen.getByText('Unverified')).toBeInTheDocument();
});
it('calls onChange when editor value changes', () => {
test('calls onChange when editor value changes', () => {
const onChange = jest.fn();
render(<SQLEditorWithValidation {...defaultProps} onChange={onChange} />);
@@ -313,7 +314,7 @@ describe('SQLEditorWithValidation', () => {
expect(onChange).toBeDefined();
});
it('calls onValidationComplete callback when provided', async () => {
test('calls onValidationComplete callback when provided', async () => {
const onValidationComplete = jest.fn();
const mockPost = SupersetClient.post as jest.MockedFunction<
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 mockPost = SupersetClient.post as jest.MockedFunction<
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 =
'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();
});
it('handles empty response gracefully', async () => {
test('handles empty response gracefully', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post
>;

View File

@@ -20,6 +20,7 @@ import fetchMock from 'fetch-mock';
import rison from 'rison';
import { tagToSelectOption, loadTags } from 'src/components/Tag/utils';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('tagToSelectOption', () => {
test('converts a Tag object with table_name to a SelectTagsValue', () => {
const tag = {
@@ -38,6 +39,7 @@ describe('tagToSelectOption', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('loadTags', () => {
beforeEach(() => {
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...');
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Tag type filtering', () => {
test('should render only custom type tags (type: 1)', async () => {
const mixedTypeTags = [

View File

@@ -38,7 +38,6 @@ export * from './ErrorMessage';
export { ImportModal, type ImportModelsModalProps } from './ImportModal';
export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary';
export * from './GenericLink';
export { FlashProvider, type FlashMessage } from './FlashProvider';
export { GridTable, type TableProps } from './GridTable';
export * from './Tag';
export * from './TagsList';

View File

@@ -122,7 +122,6 @@ export const RESERVED_DASHBOARD_URL_PARAMS: string[] = [
export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
application_root: '/',
static_assets_prefix: '',
flash_messages: [],
conf: {},
locale: 'en',
feature_flags: {},
@@ -171,6 +170,7 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
},
d3_format: DEFAULT_D3_FORMAT,
d3_time_format: DEFAULT_D3_TIME_FORMAT,
pdf_compression_level: 'MEDIUM',
};
export const DEFAULT_BOOTSTRAP_DATA: BootstrapData = {

View File

@@ -58,6 +58,7 @@ import {
NEW_ROW_ID,
} from 'src/dashboard/util/constants';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dashboardLayout actions', () => {
const mockState = {
dashboardState: {
@@ -85,8 +86,9 @@ describe('dashboardLayout actions', () => {
dashboardFilters.updateLayoutComponents.restore();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('updateComponents', () => {
it('should dispatch an updateLayout action', () => {
test('should dispatch an updateLayout action', () => {
const { getState, dispatch } = setup();
const nextComponents = { 1: {} };
const thunk = updateComponents(nextComponents);
@@ -101,7 +103,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});
@@ -115,8 +117,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('deleteComponents', () => {
it('should dispatch an deleteComponent action', () => {
test('should dispatch an deleteComponent action', () => {
const { getState, dispatch } = setup();
const thunk = deleteComponent('id', 'parentId');
thunk(dispatch, getState);
@@ -129,7 +132,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});
@@ -142,8 +145,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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 thunk1 = updateDashboardTitle('new text');
thunk1(dispatch, getState);
@@ -167,8 +171,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('createTopLevelTabs', () => {
it('should dispatch a createTopLevelTabs action', () => {
test('should dispatch a createTopLevelTabs action', () => {
const { getState, dispatch } = setup();
const dropResult = {};
const thunk = createTopLevelTabs(dropResult);
@@ -182,7 +187,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});
@@ -196,8 +201,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('deleteTopLevelTabs', () => {
it('should dispatch a deleteTopLevelTabs action', () => {
test('should dispatch a deleteTopLevelTabs action', () => {
const { getState, dispatch } = setup();
const dropResult = {};
const thunk = deleteTopLevelTabs(dropResult);
@@ -211,7 +217,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});
@@ -225,6 +231,7 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('resizeComponent', () => {
const 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({
dashboardLayout,
});
@@ -271,7 +278,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
dashboardLayout,
@@ -289,8 +296,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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 dropResult = {
source: { id: NEW_COMPONENTS_SOURCE_ID },
@@ -315,7 +323,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardLayout: {
// if 'dragging' is not only child will dispatch deleteComponent thunk
@@ -345,7 +353,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardLayout: {
present: {
@@ -374,7 +382,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardLayout: {
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 dropResult = {
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({
dashboardLayout: {
present: {
@@ -488,8 +496,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('undoLayoutAction', () => {
it('should dispatch a redux-undo .undo() action', () => {
test('should dispatch a redux-undo .undo() action', () => {
const { getState, dispatch } = setup({
dashboardLayout: { past: ['non-empty'] },
});
@@ -500,7 +509,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardLayout: { past: [] },
});
@@ -512,8 +521,9 @@ describe('dashboardLayout actions', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('redoLayoutAction', () => {
it('should dispatch a redux-undo .redo() action', () => {
test('should dispatch a redux-undo .redo() action', () => {
const { getState, dispatch } = setup();
const thunk = redoLayoutAction();
thunk(dispatch, getState);
@@ -524,7 +534,7 @@ describe('dashboardLayout actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});

View File

@@ -43,6 +43,7 @@ jest.mock('@superset-ui/core', () => ({
isFeatureEnabled: jest.fn(),
}));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dashboardState actions', () => {
const mockState = {
dashboardState: {
@@ -101,8 +102,9 @@ describe('dashboardState actions', () => {
return { getState, dispatch, state };
}
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveDashboardRequest', () => {
it('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
test('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false },
});
@@ -115,7 +117,7 @@ describe('dashboardState actions', () => {
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({
dashboardState: { hasUnsavedChanges: false },
});
@@ -144,6 +146,7 @@ describe('dashboardState actions', () => {
).toStrictEqual(mockParentsList);
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => {
beforeEach(() => {
isFeatureEnabled.mockImplementation(
@@ -155,7 +158,7 @@ describe('dashboardState actions', () => {
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 { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
@@ -174,7 +177,7 @@ describe('dashboardState actions', () => {
).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 { getState, dispatch } = setup();
const confirmedDashboardData = {

View File

@@ -274,7 +274,6 @@ export const hydrateDashboard =
superset_can_csv: findPermission('can_csv', 'Superset', roles),
common: {
// legacy, please use state.common instead
flash_messages: common?.flash_messages,
conf: common?.conf,
},
filterBarOrientation:

View File

@@ -19,6 +19,7 @@
import { render, act } from 'spec/helpers/testing-library';
import AnchorLink from 'src/dashboard/components/AnchorLink';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('AnchorLink', () => {
const props = {
id: 'CHART-123',
@@ -30,7 +31,7 @@ describe('AnchorLink', () => {
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();
jest.spyOn(document, 'getElementById').mockReturnValue({
scrollIntoView: callback,
@@ -49,7 +50,7 @@ describe('AnchorLink', () => {
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(
<AnchorLink showShortLinkButton={false} {...props} />,
{ useRedux: true },
@@ -58,7 +59,7 @@ describe('AnchorLink', () => {
expect(queryByRole('button')).not.toBeInTheDocument();
});
it('should render short link button', () => {
test('should render short link button', () => {
const { getByRole } = render(
<AnchorLink {...props} showShortLinkButton />,
{ useRedux: true },

View File

@@ -19,11 +19,9 @@
/* eslint-env browser */
import tinycolor from 'tinycolor2';
import Tabs from '@superset-ui/core/components/Tabs';
import { t, css, SupersetTheme, useTheme } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { t, css, SupersetTheme } from '@superset-ui/core';
import SliceAdder from 'src/dashboard/containers/SliceAdder';
import dashboardComponents from 'src/visualizations/presets/dashboardComponents';
import { useMemo } from 'react';
import NewColumn from '../gridComponents/new/NewColumn';
import NewDivider from '../gridComponents/new/NewDivider';
import NewHeader from '../gridComponents/new/NewHeader';
@@ -39,98 +37,83 @@ const TABS_KEYS = {
LAYOUT_ELEMENTS: 'LAYOUT_ELEMENTS',
};
const BuilderComponentPane = ({ topOffset = 0 }) => {
const theme = useTheme();
const nativeFiltersBarOpen = useSelector(
(state: any) => state.dashboardState.nativeFiltersBarOpen ?? false,
);
const tabBarStyle = useMemo(
() => ({
paddingLeft: nativeFiltersBarOpen ? 0 : theme.sizeUnit * 4,
}),
[nativeFiltersBarOpen, theme.sizeUnit],
);
return (
const BuilderComponentPane = ({ topOffset = 0 }) => (
<div
data-test="dashboard-builder-sidepane"
css={css`
position: sticky;
right: 0;
top: ${topOffset}px;
height: calc(100vh - ${topOffset}px);
width: ${BUILDER_PANE_WIDTH}px;
`}
>
<div
data-test="dashboard-builder-sidepane"
css={css`
position: sticky;
right: 0;
top: ${topOffset}px;
height: calc(100vh - ${topOffset}px);
css={(theme: SupersetTheme) => css`
position: absolute;
height: 100%;
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`
position: absolute;
line-height: inherit;
margin-top: ${theme.sizeUnit * 2}px;
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%;
& .ant-tabs-content {
height: 100%;
}
}
`}
items={[
{
key: TABS_KEYS.CHARTS,
label: t('Charts'),
children: (
<div
css={css`
height: calc(100vh - ${topOffset * 2}px);
`}
>
<SliceAdder />
</div>
),
},
{
key: TABS_KEYS.LAYOUT_ELEMENTS,
label: t('Layout elements'),
children: (
<>
<NewTabs />
<NewRow />
<NewColumn />
<NewHeader />
<NewMarkdown />
<NewDivider />
{dashboardComponents
.getAll()
.map(({ key: componentKey, metadata }) => (
<NewDynamicComponent
key={componentKey}
metadata={metadata}
componentKey={componentKey}
/>
))}
</>
),
},
]}
/>
</div>
}
`}
items={[
{
key: TABS_KEYS.CHARTS,
label: t('Charts'),
children: (
<div
css={css`
height: calc(100vh - ${topOffset * 2}px);
`}
>
<SliceAdder />
</div>
),
},
{
key: TABS_KEYS.LAYOUT_ELEMENTS,
label: t('Layout elements'),
children: (
<>
<NewTabs />
<NewRow />
<NewColumn />
<NewHeader />
<NewMarkdown />
<NewDivider />
{dashboardComponents
.getAll()
.map(({ key: componentKey, metadata }) => (
<NewDynamicComponent
key={componentKey}
metadata={metadata}
componentKey={componentKey}
/>
))}
</>
),
},
]}
/>
</div>
);
};
</div>
);
export default BuilderComponentPane;

View File

@@ -120,15 +120,12 @@ class Dashboard extends PureComponent {
this.applyCharts();
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
this.applyCharts();
}
const currentChartIds = getChartIdsFromLayout(prevProps.layout);
const nextChartIds = getChartIdsFromLayout(this.props.layout);
UNSAFE_componentWillReceiveProps(nextProps) {
const currentChartIds = getChartIdsFromLayout(this.props.layout);
const nextChartIds = getChartIdsFromLayout(nextProps.layout);
if (this.props.dashboardId !== nextProps.dashboardId) {
if (prevProps.dashboardId !== this.props.dashboardId) {
// single-page-app navigation check
return;
}
@@ -140,7 +137,7 @@ class Dashboard extends PureComponent {
newChartIds.forEach(newChartId =>
this.props.actions.addSliceToDashboard(
newChartId,
getLayoutComponentFromChartId(nextProps.layout, newChartId),
getLayoutComponentFromChartId(this.props.layout, newChartId),
),
);
} 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');
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Dashboard', () => {
const mockAddSlice = jest.fn();
const mockRemoveSlice = jest.fn();
@@ -91,18 +92,19 @@ describe('Dashboard', () => {
jest.clearAllMocks();
});
it('should render the children component', () => {
test('should render the children component', () => {
renderDashboard();
expect(screen.getByText('Test')).toBeInTheDocument();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('layout changes', () => {
const layoutWithExtraChart = {
...props.layout,
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();
rerender(
@@ -116,7 +118,7 @@ describe('Dashboard', () => {
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 nextLayout = { ...layoutWithExtraChart };
@@ -134,8 +136,9 @@ describe('Dashboard', () => {
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
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 });
rerender(
@@ -156,7 +159,7 @@ describe('Dashboard', () => {
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 });
rerender(
@@ -170,7 +173,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).not.toHaveBeenCalled();
});
it('should call refresh when native filters changed', () => {
test('should call refresh when native filters changed', () => {
getRelatedCharts.mockReturnValue([230]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -195,7 +198,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled();
});
it('should call refresh if a filter is added', () => {
test('should call refresh if a filter is added', () => {
getRelatedCharts.mockReturnValue([1]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -214,7 +217,7 @@ describe('Dashboard', () => {
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
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -233,7 +236,7 @@ describe('Dashboard', () => {
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]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -253,7 +256,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled();
});
it('should call refresh with multiple chart ids', () => {
test('should call refresh with multiple chart ids', () => {
getRelatedCharts.mockReturnValue([1, 2]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -273,7 +276,7 @@ describe('Dashboard', () => {
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]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -293,7 +296,7 @@ describe('Dashboard', () => {
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([]);
const { rerender } = renderDashboard({
activeFilters: OVERRIDE_FILTERS,

View File

@@ -83,6 +83,7 @@ jest.mock('src/dashboard/containers/DashboardGrid', () => () => (
<div data-test="mock-dashboard-grid" />
));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DashboardBuilder', () => {
let favStarStub: 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 stickyContainer = getByTestId('dashboard-content-wrapper');
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({
dashboardState: { ...mockState.dashboardState, editMode: true },
});
@@ -134,13 +135,13 @@ describe('DashboardBuilder', () => {
expect(stickyContainer).toHaveClass('dashboard dashboard--editing');
});
it('should render a DragDroppable DashboardHeader', () => {
test('should render a DragDroppable DashboardHeader', () => {
const { queryByTestId } = setup();
const header = queryByTestId('dashboard-header-container');
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({
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({
dashboardLayout: undoableDashboardLayoutWithTabs,
});
@@ -172,7 +173,7 @@ describe('DashboardBuilder', () => {
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({
dashboardLayout: undoableDashboardLayoutWithTabs,
});
@@ -188,7 +189,7 @@ describe('DashboardBuilder', () => {
).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({
dashboardLayout: undoableDashboardLayoutWithTabs,
dashboardState: {
@@ -209,13 +210,13 @@ describe('DashboardBuilder', () => {
).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 builderComponents = queryAllByTestId('mock-builder-component-pane');
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({
dashboardState: { ...mockState.dashboardState, editMode: true },
});
@@ -223,7 +224,7 @@ describe('DashboardBuilder', () => {
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 => ({
type: 'type',
arg0,
@@ -243,13 +244,13 @@ describe('DashboardBuilder', () => {
(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();
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({
dashboardState: { ...mockState.dashboardState, dashboardIsSaving: true },
});
@@ -257,7 +258,7 @@ describe('DashboardBuilder', () => {
expect(await findByTestId('loading-indicator')).toBeVisible();
});
it('should set FilterBar width by useStoredSidebarWidth', () => {
test('should set FilterBar width by useStoredSidebarWidth', () => {
const expectedValue = 200;
const setter = jest.fn();
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
@@ -274,7 +275,7 @@ describe('DashboardBuilder', () => {
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
});
it('filter panel state when featureflag is true', () => {
test('filter panel state when featureflag is true', () => {
window.featureFlags = {
[FeatureFlag.FilterBarClosedByDefault]: true,
};
@@ -294,7 +295,7 @@ describe('DashboardBuilder', () => {
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 = {
[FeatureFlag.FilterBarClosedByDefault]: false,
};
@@ -314,7 +315,7 @@ describe('DashboardBuilder', () => {
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({
showDashboard: true,
missingInitialFilters: [],
@@ -327,7 +328,7 @@ describe('DashboardBuilder', () => {
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({
showDashboard: true,
missingInitialFilters: [],
@@ -340,7 +341,7 @@ describe('DashboardBuilder', () => {
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({
showDashboard: true,
missingInitialFilters: [],

View File

@@ -54,6 +54,7 @@ buildActiveFilters({
components: dashboardWithFilter,
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('for dashboard filters', () => {
test('does not show number when there are no active filters', () => {
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', () => {
test('does not show number when there are no active filters', () => {
const store = getMockStoreWithNativeFilters();

View File

@@ -86,16 +86,6 @@ const StyledFilterCount = styled.div`
const StyledBadge = styled(Badge)`
${({ theme }) => `
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"
className="applied-count"
count={filterCount}
size="small"
showZero
/>
</StyledFilterCount>

View File

@@ -100,7 +100,7 @@ import { useHeaderActionsMenu } from './useHeaderActionsDropdownMenu';
const extensionsRegistry = getExtensionsRegistry();
const headerContainerStyle = theme => css`
border-bottom: 1px solid ${theme.colorSplit};
border-bottom: 1px solid ${theme.colorBorder};
`;
const editButtonStyle = theme => css`

View File

@@ -28,8 +28,9 @@ const setup = (overrides?: MissingChartProps) => (
<MissingChart height={100} {...overrides} />
);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('MissingChart', () => {
it('renders a .missing-chart-container', () => {
test('renders a .missing-chart-container', () => {
const rendered = render(setup());
const missingChartContainer = rendered.container.querySelector(
@@ -38,7 +39,7 @@ describe('MissingChart', () => {
expect(missingChartContainer).toBeVisible();
});
it('renders a .missing-chart-body', () => {
test('renders a .missing-chart-body', () => {
const rendered = render(setup());
const missingChartBody = rendered.container.querySelector(

Some files were not shown because too many files have changed in this diff Show More