Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Li
7c88f2d61d fix(mysql): normalize string/bytes/Decimal values before boolean conversion
- Fix critical bug where bool('0') and bool(b'0') returned True instead of False
- Add proper type normalization for strings, bytes, and Decimal values
- Convert string/bytes/Decimal to int before applying bool() for accurate MySQL boolean conversion
- Maintains existing behavior for integer values while fixing edge cases
- Addresses feedback on conversion logic in mysql.py:323-330

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 11:42:26 -07:00
Joe Li
66f6a6ce94 fix(mysql): render TINYINT(1) columns as boolean in SQL Lab and charts
Fixes MySQL TINYINT(1)/BOOLEAN columns displaying as numeric icons with 0/1 values instead of boolean representation with True/False in SQL Lab and Explore views.

## Changes

### Schema Mapping (mysql.py)
- Add precise column_type_mappings for TINYINT(1), BOOLEAN, and BOOL patterns
- Map to GenericDataType.BOOLEAN for proper metadata inspection
- Ensures dataset schemas show boolean icons for actual boolean types only

### Runtime Conversion (mysql.py)
- Implement fetch_data override with ultra-precise boolean detection
- Convert 0/1 integers to True/False for TINYINT(1) columns
- Use multiple reliable markers: FIELD_TYPE.TINY + display_size=1 OR SQLAlchemy type string
- Extract _is_boolean_column helper method for clean detection logic
- Enables pandas boolean dtype inference via extract_dataframe_dtypes

### Testing (test_mysql.py)
- Add boolean type test cases to existing parametrized tests
- Test TINYINT(1), BOOLEAN, BOOL → boolean mapping
- Test TINYINT, TINYINT(2+) → numeric mapping (preserved behavior)

## Technical Details

MySQL stores BOOLEAN as TINYINT(1) but returns 0/1 integers instead of Python booleans. This two-layer solution:
1. Maps TINYINT(1) metadata to GenericDataType.BOOLEAN for schema inspection
2. Converts query result values 0/1 → True/False for proper pandas inference

The detection uses explicit width 1 or positive SQLAlchemy type string markers to avoid mis-converting broader TINYINT columns.

Fixes: https://github.com/apache/superset/issues/35166

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 15:20:06 -07:00
335 changed files with 3385 additions and 4065 deletions

View File

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

View File

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

View File

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

View File

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

2
GPT.md
View File

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

View File

@@ -68,11 +68,7 @@ superset/
### Apache License Headers ### Apache License Headers
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header - **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead - **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
### Code Comments
- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
- **Write timeless comments** - Comments should remain accurate regardless of when they're read
## Documentation Requirements ## Documentation Requirements
@@ -102,17 +98,6 @@ superset/
- **`selectOption()`** - Select component helper - **`selectOption()`** - Select component helper
- **React Testing Library** - NO Enzyme (removed) - **React Testing Library** - NO Enzyme (removed)
### Test Structure Guidelines
- **Use `test()` instead of `describe()` and `it()`** - Follow the [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principle
- **Why**: Reduces unnecessary nesting, improves test isolation, and makes tests more readable
- **Pattern**: Write flat test files with descriptive test names that fully describe what's being tested
- **Example**: Instead of nested `describe('Component', () => { it('should render', ...) })`, use `test('Component renders correctly', ...)`
- **Benefits**:
- Each test stands alone with a clear, searchable name
- Easier to run individual tests
- Forces you to write more descriptive test names
- Reduces cognitive overhead from nested context switching
### Test Database Patterns ### Test Database Patterns
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code - **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
- **API tests**: Update expected columns when adding new model fields - **API tests**: Update expected columns when adding new model fields

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { getColorBreakpointsBuckets, getBreakPoints } from './utils'; import { getColorBreakpointsBuckets } from './utils';
import { ColorBreakpointType } from './types'; import { ColorBreakpointType } from './types';
describe('getColorBreakpointsBuckets', () => { describe('getColorBreakpointsBuckets', () => {
@@ -44,447 +44,3 @@ describe('getColorBreakpointsBuckets', () => {
expect(result).toEqual({}); expect(result).toEqual({});
}); });
}); });
describe('getBreakPoints', () => {
const accessor = (d: any) => d.value;
describe('automatic breakpoint generation', () => {
it('generates correct number of breakpoints for given buckets', () => {
const features = [{ value: 0 }, { value: 50 }, { value: 100 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
expect(breakPoints).toHaveLength(6); // n buckets = n+1 breakpoints
expect(breakPoints.every(bp => typeof bp === 'string')).toBe(true);
});
it('ensures data range is fully covered', () => {
// Test various data ranges to ensure min/max are always included
const testCases = [
{ data: [0, 100], buckets: 5 },
{ data: [0.1, 99.9], buckets: 4 },
{ data: [-50, 50], buckets: 10 },
{ data: [3.2, 38.7], buckets: 5 }, // Original max bug case
{ data: [3.14, 100], buckets: 5 }, // Min rounding bug case (3.14 -> 3)
{ data: [2.345, 10], buckets: 4 }, // Min rounding bug case (2.345 -> 2.35)
{ data: [0.0001, 0.0009], buckets: 3 }, // Very small numbers
{ data: [1000000, 9000000], buckets: 8 }, // Large numbers
];
testCases.forEach(({ data, buckets }) => {
const [min, max] = data;
const features = [{ value: min }, { value: max }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: String(buckets) },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// Critical: min and max must be within the breakpoint range
expect(firstBp).toBeLessThanOrEqual(min);
expect(lastBp).toBeGreaterThanOrEqual(max);
expect(breakPoints).toHaveLength(buckets + 1);
});
});
it('handles uniform distribution correctly', () => {
const features = [
{ value: 0 },
{ value: 25 },
{ value: 50 },
{ value: 75 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '4' },
features,
accessor,
);
// Check that breakpoints are evenly spaced
const numericBreakPoints = breakPoints.map(parseFloat);
const deltas = [];
for (let i = 1; i < numericBreakPoints.length; i += 1) {
deltas.push(numericBreakPoints[i] - numericBreakPoints[i - 1]);
}
// All deltas should be approximately equal
const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
deltas.forEach(delta => {
expect(delta).toBeCloseTo(avgDelta, 1);
});
});
it('handles single value datasets', () => {
const features = [{ value: 42 }, { value: 42 }, { value: 42 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(42);
expect(lastBp).toBeGreaterThanOrEqual(42);
});
it('preserves appropriate precision for different scales', () => {
const testCases = [
{ data: [0, 1], expectedMaxPrecision: 1 }, // 0.0, 0.2, 0.4...
{ data: [0, 0.1], expectedMaxPrecision: 2 }, // 0.00, 0.02...
{ data: [0, 0.01], expectedMaxPrecision: 3 }, // 0.000, 0.002...
{ data: [0, 1000], expectedMaxPrecision: 0 }, // 0, 200, 400...
];
testCases.forEach(({ data, expectedMaxPrecision }) => {
const [min, max] = data;
const features = [{ value: min }, { value: max }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
breakPoints.forEach(bp => {
const decimalPlaces = (bp.split('.')[1] || '').length;
expect(decimalPlaces).toBeLessThanOrEqual(expectedMaxPrecision);
});
});
});
it('handles negative values correctly', () => {
const features = [
{ value: -100 },
{ value: -50 },
{ value: 0 },
{ value: 50 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const numericBreakPoints = breakPoints.map(parseFloat);
expect(numericBreakPoints[0]).toBeLessThanOrEqual(-100);
expect(
numericBreakPoints[numericBreakPoints.length - 1],
).toBeGreaterThanOrEqual(100);
// Verify ascending order
for (let i = 1; i < numericBreakPoints.length; i += 1) {
expect(numericBreakPoints[i]).toBeGreaterThan(
numericBreakPoints[i - 1],
);
}
});
it('handles mixed integer and decimal values', () => {
const features = [
{ value: 1 },
{ value: 2.5 },
{ value: 3.7 },
{ value: 5 },
{ value: 8.2 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '4' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(1);
expect(lastBp).toBeGreaterThanOrEqual(8.2);
});
it('uses floor/ceil for boundary breakpoints to ensure inclusion', () => {
// Test that Math.floor and Math.ceil are used for boundaries
// This ensures all data points fall within the breakpoint range
const testCases = [
{ minValue: 3.14, maxValue: 100, buckets: 5 },
{ minValue: 2.345, maxValue: 10.678, buckets: 4 },
{ minValue: 1.67, maxValue: 5.33, buckets: 3 },
{ minValue: 0.123, maxValue: 0.987, buckets: 5 },
];
testCases.forEach(({ minValue, maxValue, buckets }) => {
const features = [{ value: minValue }, { value: maxValue }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: String(buckets) },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// First breakpoint should be floored (always <= minValue)
expect(firstBp).toBeLessThanOrEqual(minValue);
// Last breakpoint should be ceiled (always >= maxValue)
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
// All values should be within range
expect(minValue).toBeGreaterThanOrEqual(firstBp);
expect(maxValue).toBeLessThanOrEqual(lastBp);
});
});
it('prevents minimum value exclusion edge case', () => {
// Specific edge case test for minimum value exclusion
// Tests the exact scenario where rounding would exclude the min value
const features = [
{ value: 3.14 }, // This would round to 3 at precision 0
{ value: 50 },
{ value: 100 },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
// The first breakpoint must be <= 3.14 (floor behavior)
expect(firstBp).toBeLessThanOrEqual(3.14);
// Verify that 3.14 is not excluded
expect(3.14).toBeGreaterThanOrEqual(firstBp);
// The first breakpoint should be a clean floor value
expect(breakPoints[0]).toMatch(/^3(\.0*)?$/);
});
it('prevents maximum value exclusion edge case', () => {
// Specific edge case test for maximum value exclusion
// Tests the exact scenario where rounding would exclude the max value
const features = [
{ value: 0 },
{ value: 20 },
{ value: 38.7 }, // Original bug case
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// The last breakpoint must be >= 38.7 (ceil behavior)
expect(lastBp).toBeGreaterThanOrEqual(38.7);
// Verify that 38.7 is not excluded
expect(38.7).toBeLessThanOrEqual(lastBp);
// The last breakpoint should be a clean ceil value
expect(breakPoints[breakPoints.length - 1]).toMatch(/^39(\.0*)?$/);
});
});
describe('custom breakpoints', () => {
it('uses custom breakpoints when provided', () => {
const features = [{ value: 5 }, { value: 15 }, { value: 25 }];
const customBreakPoints = ['0', '10', '20', '30', '40'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '' },
features,
accessor,
);
expect(breakPoints).toEqual(['0', '10', '20', '30', '40']);
});
it('sorts custom breakpoints in ascending order', () => {
const features = [{ value: 5 }];
const customBreakPoints = ['30', '10', '0', '20'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '' },
features,
accessor,
);
expect(breakPoints).toEqual(['0', '10', '20', '30']);
});
it('ignores num_buckets when custom breakpoints are provided', () => {
const features = [{ value: 5 }];
const customBreakPoints = ['0', '50', '100'];
const breakPoints = getBreakPoints(
{ break_points: customBreakPoints, num_buckets: '10' }, // num_buckets should be ignored
features,
accessor,
);
expect(breakPoints).toEqual(['0', '50', '100']);
expect(breakPoints).toHaveLength(3); // not 11
});
});
describe('edge cases and error handling', () => {
it('returns empty array when features are undefined', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
undefined as any,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('returns empty array when features is null', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
null as any,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('returns empty array when all values are undefined', () => {
const features = [
{ value: undefined },
{ value: undefined },
{ value: undefined },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
expect(breakPoints).toEqual([]);
});
it('handles empty features array', () => {
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
[],
accessor,
);
expect(breakPoints).toEqual([]);
});
it('handles string values that can be parsed as numbers', () => {
const features = [
{ value: '10.5' },
{ value: '20.3' },
{ value: '30.7' },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '3' },
features,
(d: any) =>
typeof d.value === 'string' ? parseFloat(d.value) : d.value,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
expect(firstBp).toBeLessThanOrEqual(10.5);
expect(lastBp).toBeGreaterThanOrEqual(30.7);
});
it('uses default number of buckets when not specified', () => {
const features = [{ value: 0 }, { value: 100 }];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '' },
features,
accessor,
);
// Should use DEFAULT_NUM_BUCKETS (10)
expect(breakPoints).toHaveLength(11); // 10 buckets = 11 breakpoints
});
it('handles Infinity and -Infinity values', () => {
const features = [
{ value: -Infinity },
{ value: 0 },
{ value: Infinity },
];
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
// Should return empty array when Infinity values are present
expect(breakPoints).toEqual([]);
});
});
describe('breakpoint boundaries validation', () => {
it('ensures no data points fall outside breakpoint range', () => {
// Generate random test data
const generateRandomData = (count: number, min: number, max: number) => {
const data = [];
for (let i = 0; i < count; i += 1) {
data.push({ value: Math.random() * (max - min) + min });
}
return data;
};
// Test with various random datasets
for (let i = 0; i < 10; i += 1) {
const features = generateRandomData(20, -1000, 1000);
const minValue = Math.min(...features.map(f => f.value));
const maxValue = Math.max(...features.map(f => f.value));
const breakPoints = getBreakPoints(
{ break_points: [], num_buckets: '5' },
features,
accessor,
);
const firstBp = parseFloat(breakPoints[0]);
const lastBp = parseFloat(breakPoints[breakPoints.length - 1]);
// Every data point should fall within the breakpoint range
features.forEach(feature => {
expect(feature.value).toBeGreaterThanOrEqual(firstBp);
expect(feature.value).toBeLessThanOrEqual(lastBp);
});
// The range should be as tight as possible while including all data
expect(firstBp).toBeLessThanOrEqual(minValue);
expect(lastBp).toBeGreaterThanOrEqual(maxValue);
}
});
});
});

View File

@@ -75,35 +75,19 @@ export function getBreakPoints(
if (minValue === undefined || maxValue === undefined) { if (minValue === undefined || maxValue === undefined) {
return []; return [];
} }
// Handle Infinity values
if (!Number.isFinite(minValue) || !Number.isFinite(maxValue)) {
return [];
}
const delta = (maxValue - minValue) / numBuckets; const delta = (maxValue - minValue) / numBuckets;
const precision = const precision =
delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta))); delta === 0 ? 0 : Math.max(0, Math.ceil(Math.log10(1 / delta)));
const extraBucket =
maxValue > parseFloat(maxValue.toFixed(precision)) ? 1 : 0;
const startValue =
minValue < parseFloat(minValue.toFixed(precision))
? minValue - 1
: minValue;
// Generate breakpoints return new Array(numBuckets + 1 + extraBucket)
const breakPoints = new Array(numBuckets + 1).fill(0).map((_, i) => { .fill(0)
const value = minValue + i * delta; .map((_, i) => (startValue + i * delta).toFixed(precision));
// For the first breakpoint, floor to ensure minimum is included
if (i === 0) {
const scale = Math.pow(10, precision);
return (Math.floor(minValue * scale) / scale).toFixed(precision);
}
// For the last breakpoint, ceil to ensure maximum is included
if (i === numBuckets) {
const scale = Math.pow(10, precision);
return (Math.ceil(maxValue * scale) / scale).toFixed(precision);
}
// For middle breakpoints, use standard rounding
return value.toFixed(precision);
});
return breakPoints;
} }
return formDataBreakPoints.sort( return formDataBreakPoints.sort(
@@ -162,10 +146,7 @@ export function getBreakPointColorScaler(
scaler = scaleThreshold<number, string>() scaler = scaleThreshold<number, string>()
.domain(points) .domain(points)
.range(bucketedColors); .range(bucketedColors);
// Only mask values that are strictly outside the min/max bounds maskPoint = value => !!value && (value > points[n] || value < points[0]);
// Include values equal to the max breakpoint
maskPoint = value =>
!!value && (value > points[points.length - 1] || value < points[0]);
} else { } else {
// interpolate colors linearly // interpolate colors linearly
const linearScaleDomain = extent(features, accessor); const linearScaleDomain = extent(features, accessor);

View File

@@ -322,6 +322,12 @@ export default function transformProps(
primarySeries.add(seriesOption.id as string); primarySeries.add(seriesOption.id as string);
} }
}; };
rawSeriesA.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndex),
);
rawSeriesB.forEach(seriesOption =>
mapSeriesIdToAxis(seriesOption, yAxisIndexB),
);
const showValueIndexesA = extractShowValueIndexes(rawSeriesA, { const showValueIndexesA = extractShowValueIndexes(rawSeriesA, {
stack, stack,
onlyTotal, onlyTotal,
@@ -454,11 +460,7 @@ export default function transformProps(
theme, theme,
}, },
); );
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
series.push(transformedSeries);
mapSeriesIdToAxis(transformedSeries, yAxisIndex);
}
}); });
rawSeriesB.forEach(entry => { rawSeriesB.forEach(entry => {
@@ -526,11 +528,7 @@ export default function transformProps(
theme, theme,
}, },
); );
if (transformedSeries) series.push(transformedSeries);
if (transformedSeries) {
series.push(transformedSeries);
mapSeriesIdToAxis(transformedSeries, yAxisIndexB);
}
}); });
// default to 0-100% range when doing row-level contribution chart // default to 0-100% range when doing row-level contribution chart

View File

@@ -49,6 +49,7 @@ export default {
dash_edit_perm: true, dash_edit_perm: true,
dash_save_perm: true, dash_save_perm: true,
common: { common: {
flash_messages: [],
conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 }, conf: { SUPERSET_WEBSERVER_TIMEOUT: 60 },
}, },
filterBarOrientation: FilterBarOrientation.Vertical, filterBarOrientation: FilterBarOrientation.Vertical,

View File

@@ -1,11 +0,0 @@
{
"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,6 +338,7 @@ export function runQuery(query, runPreviewOnly) {
const postPayload = { const postPayload = {
client_id: query.id, client_id: query.id,
database_id: query.dbId, database_id: query.dbId,
json: true,
runAsync: query.runAsync, runAsync: query.runAsync,
catalog: query.catalog, catalog: query.catalog,
schema: query.schema, schema: query.schema,
@@ -955,7 +956,7 @@ export function addTable(queryEditor, tableName, catalogName, schemaName) {
const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id); const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
const table = { const table = {
dbId, dbId,
queryEditorId: queryEditor.tabViewId ?? queryEditor.id, queryEditorId: queryEditor.id,
catalog: catalogName, catalog: catalogName,
schema: schemaName, schema: schemaName,
name: tableName, name: tableName,

View File

@@ -49,7 +49,6 @@ jest.mock('@superset-ui/core', () => ({
isFeatureEnabled: jest.fn(), isFeatureEnabled: jest.fn(),
})); }));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getUpToDateQuery', () => { describe('getUpToDateQuery', () => {
test('should return the up to date query editor state', () => { test('should return the up to date query editor state', () => {
const outOfUpdatedQueryEditor = { const outOfUpdatedQueryEditor = {
@@ -73,7 +72,6 @@ describe('getUpToDateQuery', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('async actions', () => { describe('async actions', () => {
const mockBigNumber = '9223372036854775807'; const mockBigNumber = '9223372036854775807';
const queryEditor = { const queryEditor = {
@@ -102,7 +100,6 @@ describe('async actions', () => {
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`); fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveQuery', () => { describe('saveQuery', () => {
const saveQueryEndpoint = 'glob:*/api/v1/saved_query/'; const saveQueryEndpoint = 'glob:*/api/v1/saved_query/';
fetchMock.post(saveQueryEndpoint, { results: { json: {} } }); fetchMock.post(saveQueryEndpoint, { results: { json: {} } });
@@ -112,7 +109,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState); return request(dispatch, () => initialState);
}; };
test('posts to the correct url', () => { it('posts to the correct url', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore(initialState); const store = mockStore(initialState);
@@ -121,7 +118,7 @@ describe('async actions', () => {
}); });
}); });
test('posts the correct query object', () => { it('posts the correct query object', () => {
const store = mockStore(initialState); const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => { return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
const call = fetchMock.calls(saveQueryEndpoint)[0]; const call = fetchMock.calls(saveQueryEndpoint)[0];
@@ -134,7 +131,7 @@ describe('async actions', () => {
}); });
}); });
test('calls 3 dispatch actions', () => { it('calls 3 dispatch actions', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -142,7 +139,7 @@ describe('async actions', () => {
}); });
}); });
test('calls QUERY_EDITOR_SAVED after making a request', () => { it('calls QUERY_EDITOR_SAVED after making a request', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -150,7 +147,7 @@ describe('async actions', () => {
}); });
}); });
test('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => { it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore(initialState); const store = mockStore(initialState);
@@ -166,7 +163,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('formatQuery', () => { describe('formatQuery', () => {
const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/'; const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
const expectedSql = 'SELECT 1'; const expectedSql = 'SELECT 1';
@@ -183,7 +179,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('fetchQueryResults', () => { describe('fetchQueryResults', () => {
const makeRequest = () => { const makeRequest = () => {
const store = mockStore(initialState); const store = mockStore(initialState);
@@ -191,7 +186,7 @@ describe('async actions', () => {
return request(dispatch, store.getState); return request(dispatch, store.getState);
}; };
test('makes the fetch request', () => { it('makes the fetch request', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -199,7 +194,7 @@ describe('async actions', () => {
}); });
}); });
test('calls requestQueryResults', () => { it('calls requestQueryResults', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -207,7 +202,7 @@ describe('async actions', () => {
}); });
}); });
test.skip('parses large number result without losing precision', () => it.skip('parses large number result without losing precision', () =>
makeRequest().then(() => { makeRequest().then(() => {
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1); expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2); expect(dispatch.callCount).toBe(2);
@@ -216,7 +211,7 @@ describe('async actions', () => {
); );
})); }));
test('calls querySuccess on fetch success', () => { it('calls querySuccess on fetch success', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore({}); const store = mockStore({});
@@ -231,7 +226,7 @@ describe('async actions', () => {
}); });
}); });
test('calls queryFailed on fetch error', () => { it('calls queryFailed on fetch error', () => {
expect.assertions(1); expect.assertions(1);
fetchMock.get( fetchMock.get(
@@ -253,14 +248,13 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runQuery without query params', () => { describe('runQuery without query params', () => {
const makeRequest = () => { const makeRequest = () => {
const request = actions.runQuery(query); const request = actions.runQuery(query);
return request(dispatch, () => initialState); return request(dispatch, () => initialState);
}; };
test('makes the fetch request', () => { it('makes the fetch request', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -268,7 +262,7 @@ describe('async actions', () => {
}); });
}); });
test('calls startQuery', () => { it('calls startQuery', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -276,7 +270,7 @@ describe('async actions', () => {
}); });
}); });
test.skip('parses large number result without losing precision', () => it.skip('parses large number result without losing precision', () =>
makeRequest().then(() => { makeRequest().then(() => {
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1); expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2); expect(dispatch.callCount).toBe(2);
@@ -285,7 +279,7 @@ describe('async actions', () => {
); );
})); }));
test('calls querySuccess on fetch success', () => { it('calls querySuccess on fetch success', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore({}); const store = mockStore({});
@@ -299,7 +293,7 @@ describe('async actions', () => {
}); });
}); });
test('calls queryFailed on fetch error and logs the error details', () => { it('calls queryFailed on fetch error and logs the error details', () => {
expect.assertions(2); expect.assertions(2);
fetchMock.post( fetchMock.post(
@@ -330,7 +324,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runQuery with query params', () => { describe('runQuery with query params', () => {
const { location } = window; const { location } = window;
@@ -349,7 +342,7 @@ describe('async actions', () => {
return request(dispatch, () => initialState); return request(dispatch, () => initialState);
}; };
test('makes the fetch request', async () => { it('makes the fetch request', async () => {
const runQueryEndpointWithParams = const runQueryEndpointWithParams =
'glob:*/api/v1/sqllab/execute/?foo=bar'; 'glob:*/api/v1/sqllab/execute/?foo=bar';
fetchMock.post( fetchMock.post(
@@ -362,9 +355,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('reRunQuery', () => { describe('reRunQuery', () => {
test('creates new query with a new id', () => { it('creates new query with a new id', () => {
const id = 'id'; const id = 'id';
const state = { const state = {
sqlLab: { sqlLab: {
@@ -380,7 +372,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('postStopQuery', () => { describe('postStopQuery', () => {
const stopQueryEndpoint = 'glob:*/api/v1/query/stop'; const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
fetchMock.post(stopQueryEndpoint, {}); fetchMock.post(stopQueryEndpoint, {});
@@ -394,7 +385,7 @@ describe('async actions', () => {
return request(dispatch); return request(dispatch);
}; };
test('makes the fetch request', () => { it('makes the fetch request', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -402,7 +393,7 @@ describe('async actions', () => {
}); });
}); });
test('calls stopQuery', () => { it('calls stopQuery', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -410,7 +401,7 @@ describe('async actions', () => {
}); });
}); });
test('sends the correct data', () => { it('sends the correct data', () => {
expect.assertions(1); expect.assertions(1);
return makeRequest().then(() => { return makeRequest().then(() => {
@@ -421,9 +412,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('cloneQueryToNewTab', () => { describe('cloneQueryToNewTab', () => {
test('creates new query editor', () => { it('creates new query editor', () => {
expect.assertions(1); expect.assertions(1);
const id = 'id'; const id = 'id';
@@ -465,7 +455,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('popSavedQuery', () => { describe('popSavedQuery', () => {
const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get'); const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get');
const store = mockStore({}); const store = mockStore({});
@@ -519,7 +508,7 @@ describe('async actions', () => {
supersetClientGetSpy.mockRestore(); supersetClientGetSpy.mockRestore();
}); });
test('calls API endpint with correct params', async () => { it('calls API endpint with correct params', async () => {
supersetClientGetSpy.mockResolvedValue({ supersetClientGetSpy.mockResolvedValue({
json: { result: mockSavedQueryApiResponse }, json: { result: mockSavedQueryApiResponse },
}); });
@@ -531,7 +520,7 @@ describe('async actions', () => {
}); });
}); });
test('dispatches addQueryEditor with correct params on successful API call', async () => { it('dispatches addQueryEditor with correct params on successful API call', async () => {
supersetClientGetSpy.mockResolvedValue({ supersetClientGetSpy.mockResolvedValue({
json: { result: mockSavedQueryApiResponse }, json: { result: mockSavedQueryApiResponse },
}); });
@@ -558,7 +547,7 @@ describe('async actions', () => {
); );
}); });
test('should dispatch addDangerToast on API error', async () => { it('should dispatch addDangerToast on API error', async () => {
supersetClientGetSpy.mockResolvedValue(new Error()); supersetClientGetSpy.mockResolvedValue(new Error());
await makeRequest(1); await makeRequest(1);
@@ -572,9 +561,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => { describe('addQueryEditor', () => {
test('creates new query editor', () => { it('creates new query editor', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore(initialState); const store = mockStore(initialState);
@@ -594,9 +582,8 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addNewQueryEditor', () => { describe('addNewQueryEditor', () => {
test('creates new query editor with new tab name', () => { it('creates new query editor with new tab name', () => {
const store = mockStore({ const store = mockStore({
...initialState, ...initialState,
sqlLab: { sqlLab: {
@@ -634,7 +621,7 @@ describe('async actions', () => {
}); });
}); });
test('set current query editor', () => { it('set current query editor', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore(initialState); const store = mockStore(initialState);
@@ -649,9 +636,8 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('swithQueryEditor', () => { describe('swithQueryEditor', () => {
test('switch to the next tab editor', () => { it('switch to the next tab editor', () => {
const store = mockStore(initialState); const store = mockStore(initialState);
const expectedActions = [ const expectedActions = [
{ {
@@ -664,7 +650,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
test('switch to the first tab editor once it reaches the rightmost tab', () => { it('switch to the first tab editor once it reaches the rightmost tab', () => {
const store = mockStore({ const store = mockStore({
...initialState, ...initialState,
sqlLab: { sqlLab: {
@@ -687,7 +673,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
test('switch to the previous tab editor', () => { it('switch to the previous tab editor', () => {
const store = mockStore({ const store = mockStore({
...initialState, ...initialState,
sqlLab: { sqlLab: {
@@ -706,7 +692,7 @@ describe('async actions', () => {
expect(store.getActions()).toEqual(expectedActions); expect(store.getActions()).toEqual(expectedActions);
}); });
test('switch to the last tab editor once it reaches the leftmost tab', () => { it('switch to the last tab editor once it reaches the leftmost tab', () => {
const store = mockStore({ const store = mockStore({
...initialState, ...initialState,
sqlLab: { sqlLab: {
@@ -729,7 +715,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('backend sync', () => { describe('backend sync', () => {
const updateTabStateEndpoint = 'glob:*/tabstateview/*'; const updateTabStateEndpoint = 'glob:*/tabstateview/*';
fetchMock.put(updateTabStateEndpoint, {}); fetchMock.put(updateTabStateEndpoint, {});
@@ -760,9 +745,8 @@ describe('async actions', () => {
afterEach(() => fetchMock.resetHistory()); afterEach(() => fetchMock.resetHistory());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => { describe('addQueryEditor', () => {
test('creates the tab state in the local storage', () => { it('creates the tab state in the local storage', () => {
expect.assertions(2); expect.assertions(2);
const store = mockStore({}); const store = mockStore({});
@@ -785,9 +769,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('removeQueryEditor', () => { describe('removeQueryEditor', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const store = mockStore({}); const store = mockStore({});
@@ -802,9 +785,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetDb', () => { describe('queryEditorSetDb', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const dbId = 42; const dbId = 42;
@@ -821,9 +803,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetCatalog', () => { describe('queryEditorSetCatalog', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const catalog = 'public'; const catalog = 'public';
@@ -840,9 +821,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetSchema', () => { describe('queryEditorSetSchema', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const schema = 'schema'; const schema = 'schema';
@@ -859,9 +839,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAutorun', () => { describe('queryEditorSetAutorun', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const autorun = true; const autorun = true;
@@ -878,9 +857,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTitle', () => { describe('queryEditorSetTitle', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const name = 'name'; const name = 'name';
@@ -899,7 +877,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetAndSaveSql', () => { describe('queryEditorSetAndSaveSql', () => {
const sql = 'SELECT * '; const sql = 'SELECT * ';
const expectedActions = [ const expectedActions = [
@@ -909,9 +886,8 @@ describe('async actions', () => {
sql, sql,
}, },
]; ];
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with backend persistence flag on', () => { describe('with backend persistence flag on', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const store = mockStore({ const store = mockStore({
@@ -928,9 +904,8 @@ describe('async actions', () => {
}); });
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with backend persistence flag off', () => { describe('with backend persistence flag off', () => {
test('does not update the tab state in the backend', () => { it('does not update the tab state in the backend', () => {
isFeatureEnabled.mockImplementation( isFeatureEnabled.mockImplementation(
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
); );
@@ -952,9 +927,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetQueryLimit', () => { describe('queryEditorSetQueryLimit', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const queryLimit = 10; const queryLimit = 10;
@@ -973,9 +947,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('queryEditorSetTemplateParams', () => { describe('queryEditorSetTemplateParams', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(1); expect.assertions(1);
const templateParams = '{"foo": "bar"}'; const templateParams = '{"foo": "bar"}';
@@ -995,9 +968,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addTable', () => { describe('addTable', () => {
test('dispatches table state from unsaved change', () => { it('dispatches table state from unsaved change', () => {
const tableName = 'table'; const tableName = 'table';
const catalogName = null; const catalogName = null;
const schemaName = 'schema'; const schemaName = 'schema';
@@ -1030,90 +1002,10 @@ describe('async actions', () => {
}), }),
); );
}); });
test('uses tabViewId when available', () => {
const tableName = 'table';
const catalogName = null;
const schemaName = 'schema';
const expectedDbId = 473892;
const tabViewId = '123';
const queryWithTabViewId = { ...query, tabViewId };
const store = mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: query.id,
dbId: expectedDbId,
},
},
});
const request = actions.addTable(
queryWithTabViewId,
tableName,
catalogName,
schemaName,
);
request(store.dispatch, store.getState);
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
table: expect.objectContaining({
name: tableName,
catalog: catalogName,
schema: schemaName,
dbId: expectedDbId,
queryEditorId: tabViewId, // Should use tabViewId, not id
}),
}),
);
});
test('falls back to id when tabViewId is not available', () => {
const tableName = 'table';
const catalogName = null;
const schemaName = 'schema';
const expectedDbId = 473892;
const queryWithoutTabViewId = { ...query, tabViewId: undefined };
const store = mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: query.id,
dbId: expectedDbId,
},
},
});
const request = actions.addTable(
queryWithoutTabViewId,
tableName,
catalogName,
schemaName,
);
request(store.dispatch, store.getState);
expect(store.getActions()[0]).toEqual(
expect.objectContaining({
table: expect.objectContaining({
name: tableName,
catalog: catalogName,
schema: schemaName,
dbId: expectedDbId,
queryEditorId: query.id, // Should use id when tabViewId is not available
}),
}),
);
});
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('syncTable', () => { describe('syncTable', () => {
test('updates the table schema state in the backend', () => { it('updates the table schema state in the backend', () => {
expect.assertions(4); expect.assertions(4);
const tableName = 'table'; const tableName = 'table';
@@ -1136,7 +1028,6 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runTablePreviewQuery', () => { describe('runTablePreviewQuery', () => {
const results = { const results = {
data: mockBigNumber, data: mockBigNumber,
@@ -1167,7 +1058,7 @@ describe('async actions', () => {
fetchMock.resetHistory(); fetchMock.resetHistory();
}); });
test('updates and runs data preview query when configured', () => { it('updates and runs data preview query when configured', () => {
expect.assertions(3); expect.assertions(3);
const expectedActionTypes = [ const expectedActionTypes = [
@@ -1191,7 +1082,7 @@ describe('async actions', () => {
}); });
}); });
test('runs data preview query only', () => { it('runs data preview query only', () => {
const expectedActionTypes = [ const expectedActionTypes = [
actions.START_QUERY, // runQuery (data preview) actions.START_QUERY, // runQuery (data preview)
actions.QUERY_SUCCESS, // querySuccess actions.QUERY_SUCCESS, // querySuccess
@@ -1216,9 +1107,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('expandTable', () => { describe('expandTable', () => {
test('updates the table schema state in the backend', () => { it('updates the table schema state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const table = { id: 1 }; const table = { id: 1 };
@@ -1236,9 +1126,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('collapseTable', () => { describe('collapseTable', () => {
test('updates the table schema state in the backend', () => { it('updates the table schema state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const table = { id: 1 }; const table = { id: 1 };
@@ -1256,9 +1145,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('removeTables', () => { describe('removeTables', () => {
test('updates the table schema state in the backend', () => { it('updates the table schema state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const table = { id: 1, initialized: true }; const table = { id: 1, initialized: true };
@@ -1275,7 +1163,7 @@ describe('async actions', () => {
}); });
}); });
test('deletes multiple tables and updates the table schema state in the backend', () => { it('deletes multiple tables and updates the table schema state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const tables = [ const tables = [
@@ -1295,7 +1183,7 @@ describe('async actions', () => {
}); });
}); });
test('only updates the initialized table schema state in the backend', () => { it('only updates the initialized table schema state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const tables = [{ id: 1 }, { id: 2, initialized: true }]; const tables = [{ id: 1 }, { id: 2, initialized: true }];
@@ -1313,9 +1201,8 @@ describe('async actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('syncQueryEditor', () => { describe('syncQueryEditor', () => {
test('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(3); expect.assertions(3);
const results = { const results = {

View File

@@ -68,13 +68,12 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
}, },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('AceEditorWrapper', () => { describe('AceEditorWrapper', () => {
beforeEach(() => { beforeEach(() => {
(FullSQLEditor as any as jest.Mock).mockClear(); (FullSQLEditor as any as jest.Mock).mockClear();
}); });
test('renders ace editor including sql value', async () => { it('renders ace editor including sql value', async () => {
const store = createStore(initialState, reducerIndex); const store = createStore(initialState, reducerIndex);
const { getByTestId } = setup(defaultQueryEditor, store); const { getByTestId } = setup(defaultQueryEditor, store);
await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
@@ -84,7 +83,7 @@ describe('AceEditorWrapper', () => {
); );
}); });
test('renders current sql for unrelated unsaved changes', () => { it('renders current sql for unrelated unsaved changes', () => {
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE'; const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
const store = createStore( const store = createStore(
{ {
@@ -109,7 +108,7 @@ describe('AceEditorWrapper', () => {
); );
}); });
test('skips rerendering for updating cursor position', () => { it('skips rerendering for updating cursor position', () => {
const store = createStore(initialState, reducerIndex); const store = createStore(initialState, reducerIndex);
setup(defaultQueryEditor, store); setup(defaultQueryEditor, store);

View File

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

View File

@@ -44,7 +44,6 @@ type Params = {
dbId?: string | number; dbId?: string | number;
catalog?: string | null; catalog?: string | null;
schema?: string; schema?: string;
tabViewId?: string;
}; };
const EMPTY_LIST = [] as typeof sqlKeywords; const EMPTY_LIST = [] as typeof sqlKeywords;
@@ -60,7 +59,7 @@ const getHelperText = (value: string) =>
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();
export function useKeywords( export function useKeywords(
{ queryEditorId, dbId, catalog, schema, tabViewId }: Params, { queryEditorId, dbId, catalog, schema }: Params,
skip = false, skip = false,
) { ) {
const useCustomKeywords = extensionsRegistry.get( const useCustomKeywords = extensionsRegistry.get(
@@ -148,12 +147,7 @@ export function useKeywords(
const insertMatch = useEffectEvent((editor: Editor, data: any) => { const insertMatch = useEffectEvent((editor: Editor, data: any) => {
if (data.meta === 'table') { if (data.meta === 'table') {
dispatch( dispatch(
addTable( addTable({ id: queryEditorId, dbId }, data.value, catalog, schema),
{ id: queryEditorId, dbId, tabViewId },
data.value,
catalog,
schema,
),
); );
} }

View File

@@ -47,7 +47,6 @@ const sqlLabReducer = combineReducers({
}); });
const mockAction = {} as AnyAction; const mockAction = {} as AnyAction;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlLab App', () => { describe('SqlLab App', () => {
const middlewares = [thunk]; const middlewares = [thunk];
const mockStore = configureStore(middlewares); const mockStore = configureStore(middlewares);
@@ -61,23 +60,23 @@ describe('SqlLab App', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
test('is valid', () => { it('is valid', () => {
expect(isValidElement(<App />)).toBe(true); expect(isValidElement(<App />)).toBe(true);
}); });
test('should render', () => { it('should render', () => {
const { getByTestId } = render(<App />, { useRedux: true, store }); const { getByTestId } = render(<App />, { useRedux: true, store });
expect(getByTestId('SqlLabApp')).toBeInTheDocument(); expect(getByTestId('SqlLabApp')).toBeInTheDocument();
expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument(); expect(getByTestId('mock-tabbed-sql-editors')).toBeInTheDocument();
}); });
test('reset hotkey events on unmount', () => { it('reset hotkey events on unmount', () => {
const { unmount } = render(<App />, { useRedux: true, store }); const { unmount } = render(<App />, { useRedux: true, store });
unmount(); unmount();
expect(Mousetrap.reset).toHaveBeenCalled(); expect(Mousetrap.reset).toHaveBeenCalled();
}); });
test('logs current usage warning', () => { it('logs current usage warning', () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10; const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB + 10;
const initialState = { const initialState = {
localStorageUsageInKilobytes, localStorageUsageInKilobytes,
@@ -101,7 +100,7 @@ describe('SqlLab App', () => {
); );
}); });
test('logs current local storage usage', async () => { it('logs current local storage usage', async () => {
const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10; const localStorageUsageInKilobytes = LOCALSTORAGE_MAX_USAGE_KB - 10;
const storeExceedLocalStorage = mockStore( const storeExceedLocalStorage = mockStore(
sqlLabReducer( sqlLabReducer(

View File

@@ -21,23 +21,22 @@ import { render } from 'spec/helpers/testing-library';
import ColumnElement from 'src/SqlLab/components/ColumnElement'; import ColumnElement from 'src/SqlLab/components/ColumnElement';
import { mockedActions, table } from 'src/SqlLab/fixtures'; import { mockedActions, table } from 'src/SqlLab/fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ColumnElement', () => { describe('ColumnElement', () => {
const mockedProps = { const mockedProps = {
actions: mockedActions, actions: mockedActions,
column: table.columns[0], column: table.columns[0],
}; };
test('is valid with props', () => { it('is valid with props', () => {
expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true); expect(isValidElement(<ColumnElement {...mockedProps} />)).toBe(true);
}); });
test('renders a proper primary key', () => { it('renders a proper primary key', () => {
const { container } = render(<ColumnElement column={table.columns[0]} />); const { container } = render(<ColumnElement column={table.columns[0]} />);
expect(container.querySelector('i.fa-key')).toBeInTheDocument(); expect(container.querySelector('i.fa-key')).toBeInTheDocument();
expect( expect(
container.querySelector('[data-test="col-name"]')?.firstChild, container.querySelector('[data-test="col-name"]')?.firstChild,
).toHaveTextContent('id'); ).toHaveTextContent('id');
}); });
test('renders a multi-key column', () => { it('renders a multi-key column', () => {
const { container } = render(<ColumnElement column={table.columns[1]} />); const { container } = render(<ColumnElement column={table.columns[1]} />);
expect(container.querySelector('i.fa-link')).toBeInTheDocument(); expect(container.querySelector('i.fa-link')).toBeInTheDocument();
expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument(); expect(container.querySelector('i.fa-bookmark')).toBeInTheDocument();
@@ -45,7 +44,7 @@ describe('ColumnElement', () => {
container.querySelector('[data-test="col-name"]')?.firstChild, container.querySelector('[data-test="col-name"]')?.firstChild,
).toHaveTextContent('first_name'); ).toHaveTextContent('first_name');
}); });
test('renders a column with no keys', () => { it('renders a column with no keys', () => {
const { container } = render(<ColumnElement column={table.columns[2]} />); const { container } = render(<ColumnElement column={table.columns[2]} />);
expect(container.querySelector('i')).not.toBeInTheDocument(); expect(container.querySelector('i')).not.toBeInTheDocument();
expect( expect(

View File

@@ -53,15 +53,14 @@ const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
}, },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('EstimateQueryCostButton', () => { describe('EstimateQueryCostButton', () => {
test('renders EstimateQueryCostButton', async () => { it('renders EstimateQueryCostButton', async () => {
const { queryByText } = setup({}, mockStore(initialState)); const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Estimate cost')).toBeInTheDocument(); expect(queryByText('Estimate cost')).toBeInTheDocument();
}); });
test('renders label for selected query', async () => { it('renders label for selected query', async () => {
const { queryByText } = setup( const { queryByText } = setup(
{ queryEditorId: extraQueryEditor1.id }, { queryEditorId: extraQueryEditor1.id },
mockStore(initialState), mockStore(initialState),
@@ -70,7 +69,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate selected query cost')).toBeInTheDocument(); expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
}); });
test('renders label for selected query from unsaved', async () => { it('renders label for selected query from unsaved', async () => {
const { queryByText } = setup( const { queryByText } = setup(
{}, {},
mockStore({ mockStore({
@@ -88,7 +87,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate selected query cost')).toBeInTheDocument(); expect(queryByText('Estimate selected query cost')).toBeInTheDocument();
}); });
test('renders estimation error result', async () => { it('renders estimation error result', async () => {
const { queryByText, getByText } = setup( const { queryByText, getByText } = setup(
{}, {},
mockStore({ mockStore({
@@ -110,7 +109,7 @@ describe('EstimateQueryCostButton', () => {
expect(queryByText('Estimate error')).toBeInTheDocument(); expect(queryByText('Estimate error')).toBeInTheDocument();
}); });
test('renders estimation success result', async () => { it('renders estimation success result', async () => {
const { queryByText, getByText } = setup( const { queryByText, getByText } = setup(
{}, {},
mockStore({ mockStore({

View File

@@ -47,18 +47,17 @@ const setup = (props: Partial<ExploreCtasResultsButtonProps>, store?: Store) =>
}, },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ExploreCtasResultsButton', () => { describe('ExploreCtasResultsButton', () => {
const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm'); const postFormSpy = jest.spyOn(SupersetClientClass.prototype, 'postForm');
postFormSpy.mockImplementation(jest.fn()); postFormSpy.mockImplementation(jest.fn());
test('renders', async () => { it('renders', async () => {
const { queryByText } = setup({}, mockStore(initialState)); const { queryByText } = setup({}, mockStore(initialState));
expect(queryByText('Explore')).toBeInTheDocument(); expect(queryByText('Explore')).toBeInTheDocument();
}); });
test('visualize results', async () => { it('visualize results', async () => {
const { getByText } = setup({}, mockStore(initialState)); const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear(); postFormSpy.mockClear();
@@ -76,7 +75,7 @@ describe('ExploreCtasResultsButton', () => {
}); });
}); });
test('visualize results fails', async () => { it('visualize results fails', async () => {
const { getByText } = setup({}, mockStore(initialState)); const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear(); postFormSpy.mockClear();

View File

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

View File

@@ -43,7 +43,6 @@ const mockState = {
databases: mockDatabases, databases: mockDatabases,
}; };
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryAutoRefresh', () => { describe('QueryAutoRefresh', () => {
const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery }; const runningQueries: QueryDictionary = { [runningQuery.id]: runningQuery };
const successfulQueries: QueryDictionary = { const successfulQueries: QueryDictionary = {
@@ -63,15 +62,15 @@ describe('QueryAutoRefresh', () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
test('isQueryRunning returns true for valid running query', () => { it('isQueryRunning returns true for valid running query', () => {
expect(isQueryRunning(runningQuery)).toBe(true); expect(isQueryRunning(runningQuery)).toBe(true);
}); });
test('isQueryRunning returns false for valid not-running query', () => { it('isQueryRunning returns false for valid not-running query', () => {
expect(isQueryRunning(successfulQuery)).toBe(false); expect(isQueryRunning(successfulQuery)).toBe(false);
}); });
test('isQueryRunning returns false for invalid query', () => { it('isQueryRunning returns false for invalid query', () => {
// @ts-ignore // @ts-ignore
expect(isQueryRunning(null)).toBe(false); expect(isQueryRunning(null)).toBe(false);
// @ts-ignore // @ts-ignore
@@ -82,15 +81,15 @@ describe('QueryAutoRefresh', () => {
expect(isQueryRunning({ state: { badFormat: true } })).toBe(false); expect(isQueryRunning({ state: { badFormat: true } })).toBe(false);
}); });
test('shouldCheckForQueries is true for valid running query', () => { it('shouldCheckForQueries is true for valid running query', () => {
expect(shouldCheckForQueries(runningQueries)).toBe(true); expect(shouldCheckForQueries(runningQueries)).toBe(true);
}); });
test('shouldCheckForQueries is false for valid completed query', () => { it('shouldCheckForQueries is false for valid completed query', () => {
expect(shouldCheckForQueries(successfulQueries)).toBe(false); expect(shouldCheckForQueries(successfulQueries)).toBe(false);
}); });
test('shouldCheckForQueries is false for invalid inputs', () => { it('shouldCheckForQueries is false for invalid inputs', () => {
// @ts-ignore // @ts-ignore
expect(shouldCheckForQueries(null)).toBe(false); expect(shouldCheckForQueries(null)).toBe(false);
// @ts-ignore // @ts-ignore
@@ -110,7 +109,7 @@ describe('QueryAutoRefresh', () => {
).toBe(false); ).toBe(false);
}); });
test('Attempts to refresh when given pending query', async () => { it('Attempts to refresh when given pending query', async () => {
const store = mockStore({ sqlLab: { ...mockState } }); const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { fetchMock.get(refreshApi, {
@@ -136,7 +135,7 @@ describe('QueryAutoRefresh', () => {
); );
}); });
test('Attempts to clear inactive queries when updated queries are empty', async () => { it('Attempts to clear inactive queries when updated queries are empty', async () => {
const store = mockStore({ sqlLab: { ...mockState } }); const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { result: [] }); fetchMock.get(refreshApi, { result: [] });
@@ -165,7 +164,7 @@ describe('QueryAutoRefresh', () => {
expect(fetchMock.calls(refreshApi)).toHaveLength(1); expect(fetchMock.calls(refreshApi)).toHaveLength(1);
}); });
test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => { it('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } }); const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { fetchMock.get(refreshApi, {
@@ -192,7 +191,7 @@ describe('QueryAutoRefresh', () => {
); );
}); });
test('Does NOT Attempt to refresh when given only completed queries', async () => { it('Does NOT Attempt to refresh when given only completed queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } }); const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { fetchMock.get(refreshApi, {
@@ -220,7 +219,7 @@ describe('QueryAutoRefresh', () => {
expect(fetchMock.calls(refreshApi)).toHaveLength(0); expect(fetchMock.calls(refreshApi)).toHaveLength(0);
}); });
test('logs the failed error for async queries', async () => { it('logs the failed error for async queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } }); const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, { fetchMock.get(refreshApi, {

View File

@@ -58,9 +58,8 @@ const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
}, },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('QueryLimitSelect', () => { describe('QueryLimitSelect', () => {
test('renders current query limit size', () => { it('renders current query limit size', () => {
const queryLimit = 10; const queryLimit = 10;
const { getByText } = setup( const { getByText } = setup(
{ {
@@ -82,12 +81,12 @@ describe('QueryLimitSelect', () => {
expect(getByText(queryLimit)).toBeInTheDocument(); expect(getByText(queryLimit)).toBeInTheDocument();
}); });
test('renders default query limit for initial queryEditor', () => { it('renders default query limit for initial queryEditor', () => {
const { getByText } = setup({}, mockStore(initialState)); const { getByText } = setup({}, mockStore(initialState));
expect(getByText(defaultQueryLimit)).toBeInTheDocument(); expect(getByText(defaultQueryLimit)).toBeInTheDocument();
}); });
test('renders queryLimit from unsavedQueryEditor', () => { it('renders queryLimit from unsavedQueryEditor', () => {
const queryLimit = 10000; const queryLimit = 10000;
const { getByText } = setup( const { getByText } = setup(
{}, {},
@@ -105,7 +104,7 @@ describe('QueryLimitSelect', () => {
expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument(); expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
}); });
test('renders dropdown select', async () => { it('renders dropdown select', async () => {
const { baseElement, getAllByRole, getByRole } = setup( const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 50000 }, { maxRow: 50000 },
mockStore(initialState), mockStore(initialState),
@@ -127,7 +126,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels); expect(actualLabels).toEqual(expectedLabels);
}); });
test('renders dropdown select correctly when maxRow is less than 10', async () => { it('renders dropdown select correctly when maxRow is less than 10', async () => {
const { baseElement, getAllByRole, getByRole } = setup( const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 5 }, { maxRow: 5 },
mockStore(initialState), mockStore(initialState),
@@ -147,7 +146,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels); expect(actualLabels).toEqual(expectedLabels);
}); });
test('renders dropdown select correctly when maxRow is a multiple of 10', async () => { it('renders dropdown select correctly when maxRow is a multiple of 10', async () => {
const { baseElement, getAllByRole, getByRole } = setup( const { baseElement, getAllByRole, getByRole } = setup(
{ maxRow: 10000 }, { maxRow: 10000 },
mockStore(initialState), mockStore(initialState),
@@ -169,7 +168,7 @@ describe('QueryLimitSelect', () => {
expect(actualLabels).toEqual(expectedLabels); expect(actualLabels).toEqual(expectedLabels);
}); });
test('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => { it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
const store = mockStore(initialState); const store = mockStore(initialState);
const expectedIndex = 1; const expectedIndex = 1;
const { baseElement, getAllByRole, getByRole } = setup({}, store); const { baseElement, getAllByRole, getByRole } = setup({}, store);

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,9 +62,8 @@ jest.mock('src/explore/exploreUtils/formData', () => ({
postFormData: jest.fn(), postFormData: jest.fn(),
})); }));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SaveDatasetModal', () => { describe('SaveDatasetModal', () => {
test('renders a "Save as new" field', () => { it('renders a "Save as new" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const saveRadioBtn = screen.getByRole('radio', { const saveRadioBtn = screen.getByRole('radio', {
@@ -81,7 +80,7 @@ describe('SaveDatasetModal', () => {
expect(inputFieldText).toBeInTheDocument(); expect(inputFieldText).toBeInTheDocument();
}); });
test('renders an "Overwrite existing" field', () => { it('renders an "Overwrite existing" field', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
const overwriteRadioBtn = screen.getByRole('radio', { const overwriteRadioBtn = screen.getByRole('radio', {
@@ -97,20 +96,20 @@ describe('SaveDatasetModal', () => {
expect(placeholderText).toBeInTheDocument(); expect(placeholderText).toBeInTheDocument();
}); });
test('renders a close button', () => { it('renders a close button', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
}); });
test('renders a save button when "Save as new" is selected', () => { it('renders a save button when "Save as new" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// "Save as new" is selected when the modal opens by default // "Save as new" is selected when the modal opens by default
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
}); });
test('renders an overwrite button when "Overwrite existing" is selected', () => { it('renders an overwrite button when "Overwrite existing" is selected', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
// Click the overwrite radio button to reveal the overwrite confirmation and back buttons // Click the overwrite radio button to reveal the overwrite confirmation and back buttons
@@ -124,7 +123,7 @@ describe('SaveDatasetModal', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test('renders the overwrite button as disabled until an existing dataset is selected', async () => { it('renders the overwrite button as disabled until an existing dataset is selected', async () => {
useSelectorMock.mockReturnValue({ ...user }); useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -156,7 +155,7 @@ describe('SaveDatasetModal', () => {
expect(overwriteConfirmationBtn).toBeEnabled(); expect(overwriteConfirmationBtn).toBeEnabled();
}); });
test('renders a confirm overwrite screen when overwrite is clicked', async () => { it('renders a confirm overwrite screen when overwrite is clicked', async () => {
useSelectorMock.mockReturnValue({ ...user }); useSelectorMock.mockReturnValue({ ...user });
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
@@ -197,7 +196,7 @@ describe('SaveDatasetModal', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test('sends the schema when creating the dataset', async () => { it('sends the schema when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({}); const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch); useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user }); useSelectorMock.mockReturnValue({ ...user });
@@ -222,7 +221,7 @@ describe('SaveDatasetModal', () => {
}); });
}); });
test('sends the catalog when creating the dataset', async () => { it('sends the catalog when creating the dataset', async () => {
const dummyDispatch = jest.fn().mockResolvedValue({}); const dummyDispatch = jest.fn().mockResolvedValue({});
useDispatchMock.mockReturnValue(dummyDispatch); useDispatchMock.mockReturnValue(dummyDispatch);
useSelectorMock.mockReturnValue({ ...user }); useSelectorMock.mockReturnValue({ ...user });
@@ -253,12 +252,12 @@ describe('SaveDatasetModal', () => {
}); });
}); });
test('does not renders a checkbox button when template processing is disabled', () => { it('does not renders a checkbox button when template processing is disabled', () => {
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true }); render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
}); });
test('renders a checkbox button when template processing is enabled', () => { it('renders a checkbox button when template processing is enabled', () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true, [FeatureFlag.EnableTemplateProcessing]: true,
@@ -267,7 +266,7 @@ describe('SaveDatasetModal', () => {
expect(screen.getByRole('checkbox')).toBeInTheDocument(); expect(screen.getByRole('checkbox')).toBeInTheDocument();
}); });
test('correctly includes template parameters when template processing is enabled', () => { it('correctly includes template parameters when template processing is enabled', () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true, [FeatureFlag.EnableTemplateProcessing]: true,
@@ -302,7 +301,7 @@ describe('SaveDatasetModal', () => {
}); });
}); });
test('correctly excludes template parameters when template processing is enabled', () => { it('correctly excludes template parameters when template processing is enabled', () => {
// @ts-ignore // @ts-ignore
global.featureFlags = { global.featureFlags = {
[FeatureFlag.EnableTemplateProcessing]: true, [FeatureFlag.EnableTemplateProcessing]: true,

View File

@@ -64,9 +64,8 @@ const splitSaveBtnProps = {
const middlewares = [thunk]; const middlewares = [thunk];
const mockStore = configureStore(middlewares); const mockStore = configureStore(middlewares);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SavedQuery', () => { describe('SavedQuery', () => {
test('doesnt render save button when allows_virtual_table_explore is undefined', async () => { it('doesnt render save button when allows_virtual_table_explore is undefined', async () => {
const noRenderProps = { const noRenderProps = {
...mockedProps, ...mockedProps,
database: { database: {
@@ -85,7 +84,7 @@ describe('SavedQuery', () => {
); );
}); });
test('renders a non-split save button when allows_virtual_table_explore is not enabled', () => { it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
render(<SaveQuery {...mockedProps} />, { render(<SaveQuery {...mockedProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -96,7 +95,7 @@ describe('SavedQuery', () => {
expect(saveBtn).toBeVisible(); expect(saveBtn).toBeVisible();
}); });
test('renders a save query modal when user clicks save button', () => { it('renders a save query modal when user clicks save button', () => {
render(<SaveQuery {...mockedProps} />, { render(<SaveQuery {...mockedProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -112,7 +111,7 @@ describe('SavedQuery', () => {
expect(saveQueryModalHeader).toBeInTheDocument(); expect(saveQueryModalHeader).toBeInTheDocument();
}); });
test('renders the save query modal UI', () => { it('renders the save query modal UI', () => {
render(<SaveQuery {...mockedProps} />, { render(<SaveQuery {...mockedProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -147,7 +146,7 @@ describe('SavedQuery', () => {
expect(cancelBtn).toBeInTheDocument(); expect(cancelBtn).toBeInTheDocument();
}); });
test('renders a "save as new" and "update" button if query already exists', () => { it('renders a "save as new" and "update" button if query already exists', () => {
render(<SaveQuery {...mockedProps} />, { render(<SaveQuery {...mockedProps} />, {
useRedux: true, useRedux: true,
store: mockStore({ store: mockStore({
@@ -172,7 +171,7 @@ describe('SavedQuery', () => {
expect(updateBtn).toBeInTheDocument(); expect(updateBtn).toBeInTheDocument();
}); });
test('renders a split save button when allows_virtual_table_explore is enabled', async () => { it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -187,7 +186,7 @@ describe('SavedQuery', () => {
}); });
}); });
test('renders a save dataset modal when user clicks "save dataset" menu item', async () => { it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -206,7 +205,7 @@ describe('SavedQuery', () => {
expect(saveDatasetHeader).toBeInTheDocument(); expect(saveDatasetHeader).toBeInTheDocument();
}); });
test('renders the save dataset modal UI', async () => { it('renders the save dataset modal UI', async () => {
render(<SaveQuery {...splitSaveBtnProps} />, { render(<SaveQuery {...splitSaveBtnProps} />, {
useRedux: true, useRedux: true,
store: mockStore(mockState), store: mockStore(mockState),
@@ -247,7 +246,7 @@ describe('SavedQuery', () => {
expect(overwritePlaceholderText).toBeInTheDocument(); expect(overwritePlaceholderText).toBeInTheDocument();
}); });
test('modal stays open while save is in progress and closes after completion', async () => { it('modal stays open while save is in progress and closes after completion', async () => {
let resolveSave: () => void; let resolveSave: () => void;
const savePromise = new Promise<void>(resolve => { const savePromise = new Promise<void>(resolve => {
resolveSave = resolve; resolveSave = resolve;
@@ -291,7 +290,7 @@ describe('SavedQuery', () => {
expect(mockOnSave).toHaveBeenCalledTimes(1); expect(mockOnSave).toHaveBeenCalledTimes(1);
}); });
test('handles save with a new tab that has no changes', async () => { it('handles save with a new tab that has no changes', async () => {
const mockOnSave = jest.fn().mockResolvedValue(undefined); const mockOnSave = jest.fn().mockResolvedValue(undefined);
// Mock state for a new tab with default SQL // Mock state for a new tab with default SQL

View File

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

View File

@@ -149,7 +149,6 @@ const createStore = (initState: object) =>
getDefaultMiddleware().concat(api.middleware, logAction), getDefaultMiddleware().concat(api.middleware, logAction),
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditor', () => { describe('SqlEditor', () => {
beforeAll(() => { beforeAll(() => {
jest.setTimeout(30000); jest.setTimeout(30000);
@@ -193,7 +192,7 @@ describe('SqlEditor', () => {
}); });
}); });
test('does not render SqlEditor if no db selected', async () => { it('does not render SqlEditor if no db selected', async () => {
const queryEditor = initialState.sqlLab.queryEditors[2]; const queryEditor = initialState.sqlLab.queryEditors[2];
const { findByText } = setup({ ...mockedProps, queryEditor }, store); const { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect( expect(
@@ -201,7 +200,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test('renders db unavailable message', async () => { it('renders db unavailable message', async () => {
const queryEditor = initialState.sqlLab.queryEditors[1]; const queryEditor = initialState.sqlLab.queryEditors[1];
const { findByText } = setup({ ...mockedProps, queryEditor }, store); const { findByText } = setup({ ...mockedProps, queryEditor }, store);
expect( expect(
@@ -211,7 +210,7 @@ describe('SqlEditor', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test('render a SqlEditorLeftBar', async () => { it('render a SqlEditorLeftBar', async () => {
const { getByTestId, unmount } = setup(mockedProps, store); const { getByTestId, unmount } = setup(mockedProps, store);
await waitFor( await waitFor(
@@ -223,7 +222,7 @@ describe('SqlEditor', () => {
}, 15000); }, 15000);
// Update other similar tests with timeouts // Update other similar tests with timeouts
test('render an AceEditorWrapper', async () => { it('render an AceEditorWrapper', async () => {
const { findByTestId, unmount } = setup(mockedProps, store); const { findByTestId, unmount } = setup(mockedProps, store);
await waitFor( await waitFor(
@@ -234,7 +233,7 @@ describe('SqlEditor', () => {
unmount(); unmount();
}, 15000); }, 15000);
test('skip rendering an AceEditorWrapper when the current tab is inactive', async () => { it('skip rendering an AceEditorWrapper when the current tab is inactive', async () => {
const { findByTestId, queryByTestId } = setup( const { findByTestId, queryByTestId } = setup(
{ {
...mockedProps, ...mockedProps,
@@ -246,7 +245,7 @@ describe('SqlEditor', () => {
expect(queryByTestId('react-ace')).not.toBeInTheDocument(); expect(queryByTestId('react-ace')).not.toBeInTheDocument();
}); });
test('avoids rerendering EditorLeftBar and ResultSet while typing', async () => { it('avoids rerendering EditorLeftBar and ResultSet while typing', async () => {
const { findByTestId } = setup(mockedProps, store); const { findByTestId } = setup(mockedProps, store);
const editor = await findByTestId('react-ace'); const editor = await findByTestId('react-ace');
const sql = 'select *'; const sql = 'select *';
@@ -261,7 +260,7 @@ describe('SqlEditor', () => {
expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane); expect(ResultSet).toHaveBeenCalledTimes(renderCountForSouthPane);
}); });
test('renders sql from unsaved change', async () => { it('renders sql from unsaved change', async () => {
const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE'; const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
store = createStore({ store = createStore({
...initialState, ...initialState,
@@ -294,12 +293,12 @@ describe('SqlEditor', () => {
expect(editor).toHaveValue(expectedSql); expect(editor).toHaveValue(expectedSql);
}); });
test('render a SouthPane', async () => { it('render a SouthPane', async () => {
const { findByTestId } = setup(mockedProps, store); const { findByTestId } = setup(mockedProps, store);
expect(await findByTestId('mock-result-set')).toBeInTheDocument(); expect(await findByTestId('mock-result-set')).toBeInTheDocument();
}); });
test('runs query action with ctas false', async () => { it('runs query action with ctas false', async () => {
store = createStore({ store = createStore({
...initialState, ...initialState,
sqlLab: { sqlLab: {
@@ -339,7 +338,7 @@ describe('SqlEditor', () => {
); );
}); });
test('render a Limit Dropdown', async () => { it('render a Limit Dropdown', async () => {
const defaultQueryLimit = 101; const defaultQueryLimit = 101;
const updatedProps = { ...mockedProps, defaultQueryLimit }; const updatedProps = { ...mockedProps, defaultQueryLimit };
const { findByText } = setup(updatedProps, store); const { findByText } = setup(updatedProps, store);
@@ -347,7 +346,7 @@ describe('SqlEditor', () => {
expect(await findByText('10 000')).toBeInTheDocument(); expect(await findByText('10 000')).toBeInTheDocument();
}); });
test('renders an Extension if provided', async () => { it('renders an Extension if provided', async () => {
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();
extensionsRegistry.set('sqleditor.extension.form', () => ( extensionsRegistry.set('sqleditor.extension.form', () => (
@@ -361,7 +360,6 @@ describe('SqlEditor', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with EstimateQueryCost enabled', () => { describe('with EstimateQueryCost enabled', () => {
beforeEach(() => { beforeEach(() => {
mockIsFeatureEnabled.mockImplementation( mockIsFeatureEnabled.mockImplementation(
@@ -372,7 +370,7 @@ describe('SqlEditor', () => {
mockIsFeatureEnabled.mockClear(); mockIsFeatureEnabled.mockClear();
}); });
test('sends the catalog and schema to the endpoint', async () => { it('sends the catalog and schema to the endpoint', async () => {
const estimateApi = 'http://localhost/api/v1/sqllab/estimate/'; const estimateApi = 'http://localhost/api/v1/sqllab/estimate/';
fetchMock.post(estimateApi, {}); fetchMock.post(estimateApi, {});
@@ -438,7 +436,6 @@ describe('SqlEditor', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with SqllabBackendPersistence enabled', () => { describe('with SqllabBackendPersistence enabled', () => {
beforeEach(() => { beforeEach(() => {
mockIsFeatureEnabled.mockImplementation( mockIsFeatureEnabled.mockImplementation(
@@ -449,7 +446,7 @@ describe('SqlEditor', () => {
mockIsFeatureEnabled.mockClear(); mockIsFeatureEnabled.mockClear();
}); });
test('should render loading state when its Editor is not loaded', async () => { it('should render loading state when its Editor is not loaded', async () => {
const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`; const switchTabApi = `glob:*/tabstateview/${defaultQueryEditor.id}/activate`;
fetchMock.post(switchTabApi, {}); fetchMock.post(switchTabApi, {});
const { getByTestId } = setup( const { getByTestId } = setup(

View File

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

View File

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

View File

@@ -56,16 +56,15 @@ const setup = (queryEditor: QueryEditor, store?: Store) =>
...(store && { store }), ...(store && { store }),
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SqlEditorTabHeader', () => { describe('SqlEditorTabHeader', () => {
test('renders name', () => { it('renders name', () => {
const { queryByText } = setup(defaultQueryEditor, mockStore(initialState)); const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument(); expect(queryByText(defaultQueryEditor.name)).toBeInTheDocument();
expect(queryByText(extraQueryEditor1.name)).not.toBeInTheDocument(); expect(queryByText(extraQueryEditor1.name)).not.toBeInTheDocument();
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument(); expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
}); });
test('renders name from unsaved changes', () => { it('renders name from unsaved changes', () => {
const expectedTitle = 'updated title'; const expectedTitle = 'updated title';
const { queryByText } = setup( const { queryByText } = setup(
defaultQueryEditor, defaultQueryEditor,
@@ -86,7 +85,7 @@ describe('SqlEditorTabHeader', () => {
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument(); expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
}); });
test('renders current name for unrelated unsaved changes', () => { it('renders current name for unrelated unsaved changes', () => {
const unrelatedTitle = 'updated title'; const unrelatedTitle = 'updated title';
const { queryByText } = setup( const { queryByText } = setup(
defaultQueryEditor, defaultQueryEditor,
@@ -107,7 +106,6 @@ describe('SqlEditorTabHeader', () => {
expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument(); expect(queryByText(extraQueryEditor2.name)).not.toBeInTheDocument();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('with dropdown menus', () => { describe('with dropdown menus', () => {
let store = mockStore(); let store = mockStore();
beforeEach(async () => { beforeEach(async () => {
@@ -118,7 +116,7 @@ describe('SqlEditorTabHeader', () => {
userEvent.click(dropdown); userEvent.click(dropdown);
}); });
test('should dispatch removeQueryEditor action', async () => { it('should dispatch removeQueryEditor action', async () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(), expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
); );
@@ -134,7 +132,7 @@ describe('SqlEditorTabHeader', () => {
); );
}); });
test('should dispatch queryEditorSetTitle action', async () => { it('should dispatch queryEditorSetTitle action', async () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(), expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
); );
@@ -157,7 +155,7 @@ describe('SqlEditorTabHeader', () => {
mockPrompt.mockClear(); mockPrompt.mockClear();
}); });
test('should dispatch toggleLeftBar action', async () => { it('should dispatch toggleLeftBar action', async () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(), expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
); );
@@ -175,7 +173,7 @@ describe('SqlEditorTabHeader', () => {
); );
}); });
test('should dispatch removeAllOtherQueryEditors action', async () => { it('should dispatch removeAllOtherQueryEditors action', async () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(), expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
); );
@@ -196,7 +194,7 @@ describe('SqlEditorTabHeader', () => {
); );
}); });
test('should dispatch cloneQueryToNewTab action', async () => { it('should dispatch cloneQueryToNewTab action', async () => {
await waitFor(() => await waitFor(() =>
expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(), expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
); );

View File

@@ -56,7 +56,6 @@ afterEach(() => {
pathStub.mockReset(); pathStub.mockReset();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('componentDidMount', () => { describe('componentDidMount', () => {
let uriStub = jest.spyOn(URI.prototype, 'search'); let uriStub = jest.spyOn(URI.prototype, 'search');
let replaceState = jest.spyOn(window.history, 'replaceState'); let replaceState = jest.spyOn(window.history, 'replaceState');

View File

@@ -135,7 +135,6 @@ test('renders preview', async () => {
); );
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('table actions', () => { describe('table actions', () => {
test('refreshes table metadata when triggered', async () => { test('refreshes table metadata when triggered', async () => {
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, { const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {

View File

@@ -64,14 +64,13 @@ const setup = (
}, },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('TemplateParamsEditor', () => { describe('TemplateParamsEditor', () => {
test('should render with a title', () => { it('should render with a title', () => {
const { container } = setup(); const { container } = setup();
expect(container.querySelector('div[role="button"]')).toBeInTheDocument(); expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
}); });
test('should open a modal with the ace editor', async () => { it('should open a modal with the ace editor', async () => {
const { container, getByTestId } = setup(); const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters')); fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => { await waitFor(() => {
@@ -79,7 +78,7 @@ describe('TemplateParamsEditor', () => {
}); });
}); });
test('renders templateParams', async () => { it('renders templateParams', async () => {
const { container, getByTestId } = setup(); const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters')); fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => { await waitFor(() => {
@@ -90,7 +89,7 @@ describe('TemplateParamsEditor', () => {
); );
}); });
test('renders code from unsaved changes', async () => { it('renders code from unsaved changes', async () => {
const expectedCode = 'custom code value'; const expectedCode = 'custom code value';
const { container, getByTestId } = setup( const { container, getByTestId } = setup(
{}, {},

View File

@@ -52,25 +52,23 @@ const apiDataWithTabState = {
latest_query: null, latest_query: null,
}, },
}; };
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getInitialState', () => { describe('getInitialState', () => {
afterEach(() => { afterEach(() => {
localStorage.clear(); localStorage.clear();
}); });
test('should output the user that is passed in', () => { it('should output the user that is passed in', () => {
expect(getInitialState(apiData).user?.userId).toEqual(1); expect(getInitialState(apiData).user?.userId).toEqual(1);
}); });
test('should return undefined instead of null for templateParams', () => { it('should return undefined instead of null for templateParams', () => {
expect( expect(
getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0] getInitialState(apiDataWithTabState).sqlLab?.queryEditors?.[0]
?.templateParams, ?.templateParams,
).toBeUndefined(); ).toBeUndefined();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dedupeTabHistory', () => { describe('dedupeTabHistory', () => {
test('should dedupe the tab history', () => { it('should dedupe the tab history', () => {
[ [
{ value: [], expected: [] }, { value: [], expected: [] },
{ {
@@ -138,9 +136,8 @@ describe('getInitialState', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dedupe tables schema', () => { describe('dedupe tables schema', () => {
test('should dedupe the table schema', () => { it('should dedupe the table schema', () => {
localStorage.setItem( localStorage.setItem(
'redux', 'redux',
JSON.stringify({ JSON.stringify({
@@ -198,7 +195,7 @@ describe('getInitialState', () => {
expect(initializedTables.map(({ id }) => id)).toEqual([1, 2, 6]); expect(initializedTables.map(({ id }) => id)).toEqual([1, 2, 6]);
}); });
test('should parse the float dttm value', () => { it('should parse the float dttm value', () => {
const startDttmInStr = '1693433503447.166992'; const startDttmInStr = '1693433503447.166992';
const endDttmInStr = '1693433503500.23132'; const endDttmInStr = '1693433503500.23132';
@@ -252,7 +249,6 @@ describe('getInitialState', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('restore unsaved changes for PERSISTENCE mode', () => { describe('restore unsaved changes for PERSISTENCE mode', () => {
const lastUpdatedTime = Date.now(); const lastUpdatedTime = Date.now();
const expectedValue = 'updated editor value'; const expectedValue = 'updated editor value';
@@ -288,7 +284,7 @@ describe('getInitialState', () => {
); );
}); });
test('restore unsaved changes for PERSISTENCE mode', () => { it('restore unsaved changes for PERSISTENCE mode', () => {
const apiDataWithLocalStorage = { const apiDataWithLocalStorage = {
...apiData, ...apiData,
active_tab: { active_tab: {
@@ -325,7 +321,7 @@ describe('getInitialState', () => {
).toEqual(apiDataWithTabState.active_tab.id.toString()); ).toEqual(apiDataWithTabState.active_tab.id.toString());
}); });
test('skip unsaved changes for expired data', () => { it('skip unsaved changes for expired data', () => {
const apiDataWithLocalStorage = { const apiDataWithLocalStorage = {
...apiData, ...apiData,
active_tab: { active_tab: {
@@ -349,7 +345,7 @@ describe('getInitialState', () => {
); );
}); });
test('skip unsaved changes for legacy cache data', () => { it('skip unsaved changes for legacy cache data', () => {
const apiDataWithLocalStorage = { const apiDataWithLocalStorage = {
...apiData, ...apiData,
active_tab: { active_tab: {

View File

@@ -23,9 +23,7 @@ import { table, initialState as mockState } from '../fixtures';
const initialState = mockState.sqlLab; const initialState = mockState.sqlLab;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('sqlLabReducer', () => { describe('sqlLabReducer', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Query editors actions', () => { describe('Query editors actions', () => {
let newState; let newState;
let defaultQueryEditor; let defaultQueryEditor;
@@ -40,12 +38,12 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, action); newState = sqlLabReducer(newState, action);
qe = newState.queryEditors.find(e => e.id === 'abcd'); qe = newState.queryEditors.find(e => e.id === 'abcd');
}); });
test('should add a query editor', () => { it('should add a query editor', () => {
expect(newState.queryEditors).toHaveLength( expect(newState.queryEditors).toHaveLength(
initialState.queryEditors.length + 1, initialState.queryEditors.length + 1,
); );
}); });
test('should merge the current unsaved changes when adding a query editor', () => { it('should merge the current unsaved changes when adding a query editor', () => {
const expectedTitle = 'new updated title'; const expectedTitle = 'new updated title';
const updateAction = { const updateAction = {
type: actions.QUERY_EDITOR_SET_TITLE, type: actions.QUERY_EDITOR_SET_TITLE,
@@ -64,7 +62,7 @@ describe('sqlLabReducer', () => {
newState.queryEditors[newState.queryEditors.length - 1].id, newState.queryEditors[newState.queryEditors.length - 1].id,
).toEqual('efgh'); ).toEqual('efgh');
}); });
test('should remove a query editor', () => { it('should remove a query editor', () => {
expect(newState.queryEditors).toHaveLength( expect(newState.queryEditors).toHaveLength(
initialState.queryEditors.length + 1, initialState.queryEditors.length + 1,
); );
@@ -77,7 +75,7 @@ describe('sqlLabReducer', () => {
initialState.queryEditors.length, initialState.queryEditors.length,
); );
}); });
test('should select the latest query editor when tabHistory is empty', () => { it('should select the latest query editor when tabHistory is empty', () => {
const currentQE = newState.queryEditors[0]; const currentQE = newState.queryEditors[0];
newState = { newState = {
...initialState, ...initialState,
@@ -96,7 +94,7 @@ describe('sqlLabReducer', () => {
); );
expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]); expect(newState.tabHistory).toEqual([initialState.queryEditors[2].id]);
}); });
test('should remove a query editor including unsaved changes', () => { it('should remove a query editor including unsaved changes', () => {
expect(newState.queryEditors).toHaveLength( expect(newState.queryEditors).toHaveLength(
initialState.queryEditors.length + 1, initialState.queryEditors.length + 1,
); );
@@ -118,7 +116,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.dbId).toBeUndefined(); expect(newState.unsavedQueryEditor.dbId).toBeUndefined();
expect(newState.unsavedQueryEditor.id).toBeUndefined(); expect(newState.unsavedQueryEditor.id).toBeUndefined();
}); });
test('should set q query editor active', () => { it('should set q query editor active', () => {
const expectedTitle = 'new updated title'; const expectedTitle = 'new updated title';
const addQueryEditorAction = { const addQueryEditorAction = {
type: actions.ADD_QUERY_EDITOR, type: actions.ADD_QUERY_EDITOR,
@@ -141,7 +139,7 @@ describe('sqlLabReducer', () => {
); );
expect(newState.queryEditors[1].name).toEqual(expectedTitle); expect(newState.queryEditors[1].name).toEqual(expectedTitle);
}); });
test('should not fail while setting DB', () => { it('should not fail while setting DB', () => {
const dbId = 9; const dbId = 9;
const action = { const action = {
type: actions.QUERY_EDITOR_SETDB, type: actions.QUERY_EDITOR_SETDB,
@@ -152,7 +150,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.dbId).toBe(dbId); expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not fail while setting schema', () => { it('should not fail while setting schema', () => {
const schema = 'foo'; const schema = 'foo';
const action = { const action = {
type: actions.QUERY_EDITOR_SET_SCHEMA, type: actions.QUERY_EDITOR_SET_SCHEMA,
@@ -163,7 +161,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.schema).toBe(schema); expect(newState.unsavedQueryEditor.schema).toBe(schema);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not fail while setting autorun', () => { it('should not fail while setting autorun', () => {
const action = { const action = {
type: actions.QUERY_EDITOR_SET_AUTORUN, type: actions.QUERY_EDITOR_SET_AUTORUN,
queryEditor: qe, queryEditor: qe,
@@ -175,7 +173,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.autorun).toBe(true); expect(newState.unsavedQueryEditor.autorun).toBe(true);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not fail while setting title', () => { it('should not fail while setting title', () => {
const title = 'Untitled Query 1'; const title = 'Untitled Query 1';
const action = { const action = {
type: actions.QUERY_EDITOR_SET_TITLE, type: actions.QUERY_EDITOR_SET_TITLE,
@@ -186,7 +184,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.name).toBe(title); expect(newState.unsavedQueryEditor.name).toBe(title);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not fail while setting Sql', () => { it('should not fail while setting Sql', () => {
const sql = 'SELECT nothing from dev_null'; const sql = 'SELECT nothing from dev_null';
const action = { const action = {
type: actions.QUERY_EDITOR_SET_SQL, type: actions.QUERY_EDITOR_SET_SQL,
@@ -197,7 +195,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.sql).toBe(sql); expect(newState.unsavedQueryEditor.sql).toBe(sql);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not fail while setting queryLimit', () => { it('should not fail while setting queryLimit', () => {
const queryLimit = 101; const queryLimit = 101;
const action = { const action = {
type: actions.QUERY_EDITOR_SET_QUERY_LIMIT, type: actions.QUERY_EDITOR_SET_QUERY_LIMIT,
@@ -208,7 +206,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit); expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should set selectedText', () => { it('should set selectedText', () => {
const selectedText = 'TEST'; const selectedText = 'TEST';
const action = { const action = {
type: actions.QUERY_EDITOR_SET_SELECTED_TEXT, type: actions.QUERY_EDITOR_SET_SELECTED_TEXT,
@@ -220,7 +218,7 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText); expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
expect(newState.unsavedQueryEditor.id).toBe(qe.id); expect(newState.unsavedQueryEditor.id).toBe(qe.id);
}); });
test('should not wiped out unsaved changes while delayed async call intercepted', () => { it('should not wiped out unsaved changes while delayed async call intercepted', () => {
const expectedSql = 'Updated SQL WORKING IN PROGRESS--'; const expectedSql = 'Updated SQL WORKING IN PROGRESS--';
const action = { const action = {
type: actions.QUERY_EDITOR_SET_SQL, type: actions.QUERY_EDITOR_SET_SQL,
@@ -241,7 +239,7 @@ describe('sqlLabReducer', () => {
interceptedAction.northPercent, interceptedAction.northPercent,
); );
}); });
test('should migrate query editor by new query editor id', () => { it('should migrate query editor by new query editor id', () => {
const { length } = newState.queryEditors; const { length } = newState.queryEditors;
const index = newState.queryEditors.findIndex(({ id }) => id === qe.id); const index = newState.queryEditors.findIndex(({ id }) => id === qe.id);
const newQueryEditor = { const newQueryEditor = {
@@ -269,7 +267,7 @@ describe('sqlLabReducer', () => {
newQueryEditor.tabViewId, newQueryEditor.tabViewId,
); );
}); });
test('should clear the destroyed query editors', () => { it('should clear the destroyed query editors', () => {
const expectedQEId = '1233289'; const expectedQEId = '1233289';
const action = { const action = {
type: actions.CLEAR_DESTROYED_QUERY_EDITOR, type: actions.CLEAR_DESTROYED_QUERY_EDITOR,
@@ -287,7 +285,6 @@ describe('sqlLabReducer', () => {
expect(newState.destroyedQueryEditors).toEqual({}); expect(newState.destroyedQueryEditors).toEqual({});
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Tables', () => { describe('Tables', () => {
let newState; let newState;
let newTable; let newTable;
@@ -300,12 +297,12 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(initialState, action); newState = sqlLabReducer(initialState, action);
newTable = newState.tables[0]; newTable = newState.tables[0];
}); });
test('should add a table', () => { it('should add a table', () => {
// Testing that beforeEach actually added the table // Testing that beforeEach actually added the table
expect(newState.tables).toHaveLength(1); expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].expanded).toBe(true); expect(newState.tables[0].expanded).toBe(true);
}); });
test('should merge the table attributes', () => { it('should merge the table attributes', () => {
// Merging the extra attribute // Merging the extra attribute
newTable.extra = true; newTable.extra = true;
const action = { const action = {
@@ -316,7 +313,7 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(1); expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].extra).toBe(true); expect(newState.tables[0].extra).toBe(true);
}); });
test('should overwrite table ID be ignored when the existing table is already initialized', () => { it('should overwrite table ID be ignored when the existing table is already initialized', () => {
const action = { const action = {
type: actions.MERGE_TABLE, type: actions.MERGE_TABLE,
table: newTable, table: newTable,
@@ -348,7 +345,7 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(1); expect(newState.tables).toHaveLength(1);
expect(newState.tables[0].id).toBe(remoteId); expect(newState.tables[0].id).toBe(remoteId);
}); });
test('should expand and collapse a table', () => { it('should expand and collapse a table', () => {
const collapseTableAction = { const collapseTableAction = {
type: actions.COLLAPSE_TABLE, type: actions.COLLAPSE_TABLE,
table: newTable, table: newTable,
@@ -362,7 +359,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, expandTableAction); newState = sqlLabReducer(newState, expandTableAction);
expect(newState.tables[0].expanded).toBe(true); expect(newState.tables[0].expanded).toBe(true);
}); });
test('should remove a table', () => { it('should remove a table', () => {
const action = { const action = {
type: actions.REMOVE_TABLES, type: actions.REMOVE_TABLES,
tables: [newTable], tables: [newTable],
@@ -371,7 +368,6 @@ describe('sqlLabReducer', () => {
expect(newState.tables).toHaveLength(0); expect(newState.tables).toHaveLength(0);
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Run Query', () => { describe('Run Query', () => {
const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439'; const DENORMALIZED_CHANGED_ON = '2023-06-26T07:53:05.439';
const CHANGED_ON_TIMESTAMP = 1687765985439; const CHANGED_ON_TIMESTAMP = 1687765985439;
@@ -389,7 +385,7 @@ describe('sqlLabReducer', () => {
sqlEditorId: 'dfsadfs', sqlEditorId: 'dfsadfs',
}; };
}); });
test('should start a query', () => { it('should start a query', () => {
const action = { const action = {
type: actions.START_QUERY, type: actions.START_QUERY,
query: { query: {
@@ -405,7 +401,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, action); newState = sqlLabReducer(newState, action);
expect(Object.keys(newState.queries)).toHaveLength(1); expect(Object.keys(newState.queries)).toHaveLength(1);
}); });
test('should stop the query', () => { it('should stop the query', () => {
const startQueryAction = { const startQueryAction = {
type: actions.START_QUERY, type: actions.START_QUERY,
query, query,
@@ -419,7 +415,7 @@ describe('sqlLabReducer', () => {
const q = newState.queries[Object.keys(newState.queries)[0]]; const q = newState.queries[Object.keys(newState.queries)[0]];
expect(q.state).toBe('stopped'); expect(q.state).toBe('stopped');
}); });
test('should remove a query', () => { it('should remove a query', () => {
const startQueryAction = { const startQueryAction = {
type: actions.START_QUERY, type: actions.START_QUERY,
query, query,
@@ -432,7 +428,7 @@ describe('sqlLabReducer', () => {
newState = sqlLabReducer(newState, removeQueryAction); newState = sqlLabReducer(newState, removeQueryAction);
expect(Object.keys(newState.queries)).toHaveLength(0); expect(Object.keys(newState.queries)).toHaveLength(0);
}); });
test('should refresh queries when polling returns new results', () => { it('should refresh queries when polling returns new results', () => {
const startDttmInStr = '1693433503447.166992'; const startDttmInStr = '1693433503447.166992';
const endDttmInStr = '1693433503500.23132'; const endDttmInStr = '1693433503500.23132';
newState = sqlLabReducer( newState = sqlLabReducer(
@@ -453,7 +449,7 @@ describe('sqlLabReducer', () => {
expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr)); expect(newState.queries.abcd.endDttm).toBe(Number(endDttmInStr));
expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP); expect(newState.queriesLastUpdate).toBe(CHANGED_ON_TIMESTAMP);
}); });
test('should skip refreshing queries when polling contains existing results', () => { it('should skip refreshing queries when polling contains existing results', () => {
const completedQuery = { const completedQuery = {
...query, ...query,
extra: { extra: {
@@ -482,11 +478,10 @@ describe('sqlLabReducer', () => {
expect(newState.queries.abcd).toBe(query); expect(newState.queries.abcd).toBe(query);
expect(newState.queries.def).toBe(completedQuery); expect(newState.queries.def).toBe(completedQuery);
}); });
test('should refresh queries when polling returns empty', () => { it('should refresh queries when polling returns empty', () => {
newState = sqlLabReducer(newState, actions.refreshQueries({})); newState = sqlLabReducer(newState, actions.refreshQueries({}));
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('CLEAR_INACTIVE_QUERIES', () => { describe('CLEAR_INACTIVE_QUERIES', () => {
let newState; let newState;
let query; let query;
@@ -501,7 +496,7 @@ describe('sqlLabReducer', () => {
cached: false, cached: false,
}; };
}); });
test('updates queries that have already been completed', () => { it('updates queries that have already been completed', () => {
newState = sqlLabReducer( newState = sqlLabReducer(
{ {
...newState, ...newState,

View File

@@ -29,7 +29,6 @@ import {
} from 'src/SqlLab/constants'; } from 'src/SqlLab/constants';
import { queries, defaultQueryEditor } from '../fixtures'; import { queries, defaultQueryEditor } from '../fixtures';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('reduxStateToLocalStorageHelper', () => { describe('reduxStateToLocalStorageHelper', () => {
const queriesObj: Record<string, any> = {}; const queriesObj: Record<string, any> = {};
beforeEach(() => { beforeEach(() => {
@@ -38,7 +37,7 @@ describe('reduxStateToLocalStorageHelper', () => {
}); });
}); });
test('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => { it('should empty query.results if query.startDttm is > LOCALSTORAGE_MAX_QUERY_AGE_MS', () => {
// make sure sample data contains old query // make sure sample data contains old query
const oldQuery = queries[0]; const oldQuery = queries[0];
const { id, startDttm } = oldQuery; const { id, startDttm } = oldQuery;
@@ -52,7 +51,7 @@ describe('reduxStateToLocalStorageHelper', () => {
expect(emptiedQuery[id].results).toEqual({}); expect(emptiedQuery[id].results).toEqual({});
}); });
test('should empty query.results if query,.results size is greater than LOCALSTORAGE_MAX_QUERY_RESULTS_KB', () => { it('should empty query.results if query,.results size is greater than LOCALSTORAGE_MAX_QUERY_RESULTS_KB', () => {
const reasonableSizeQuery = { const reasonableSizeQuery = {
...queries[0], ...queries[0],
startDttm: Date.now(), startDttm: Date.now(),
@@ -84,7 +83,7 @@ describe('reduxStateToLocalStorageHelper', () => {
); );
}); });
test('should only return selected keys for query editor', () => { it('should only return selected keys for query editor', () => {
const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }]; const queryEditors = [{ ...defaultQueryEditor, dummy: 'value' }];
expect(Object.keys(queryEditors[0])).toContain('dummy'); expect(Object.keys(queryEditors[0])).toContain('dummy');

View File

@@ -29,14 +29,13 @@ const emptyEditor = {
remoteId: null, remoteId: null,
}; };
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('newQueryTabName', () => { describe('newQueryTabName', () => {
test("should return default title if queryEditor's length is 0", () => { it("should return default title if queryEditor's length is 0", () => {
const defaultTitle = 'default title'; const defaultTitle = 'default title';
const title = newQueryTabName([], defaultTitle); const title = newQueryTabName([], defaultTitle);
expect(title).toEqual(defaultTitle); expect(title).toEqual(defaultTitle);
}); });
test('should return next available number if there are unsaved editors', () => { it('should return next available number if there are unsaved editors', () => {
const untitledQueryText = 'Untitled Query'; const untitledQueryText = 'Untitled Query';
const unsavedEditors = [ const unsavedEditors = [
{ ...emptyEditor, name: `${untitledQueryText} 1` }, { ...emptyEditor, name: `${untitledQueryText} 1` },

View File

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

View File

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

View File

@@ -42,7 +42,6 @@ const ERROR_MESSAGE_COMPONENT = (props: ErrorMessageComponentProps) => (
</> </>
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ChartErrorMessage', () => { describe('ChartErrorMessage', () => {
const defaultProps = { const defaultProps = {
chartId: 1, chartId: 1,
@@ -50,7 +49,7 @@ describe('ChartErrorMessage', () => {
source: 'test_source' as ChartSource, source: 'test_source' as ChartSource,
}; };
test('renders the default error message when error is null', () => { it('renders the default error message when error is null', () => {
mockUseChartOwnerNames.mockReturnValue({ mockUseChartOwnerNames.mockReturnValue({
result: null, result: null,
status: ResourceStatus.Loading, status: ResourceStatus.Loading,
@@ -62,7 +61,7 @@ describe('ChartErrorMessage', () => {
expect(screen.getByText('Test subtitle')).toBeInTheDocument(); expect(screen.getByText('Test subtitle')).toBeInTheDocument();
}); });
test('renders the error message that is passed in from the error', () => { it('renders the error message that is passed in from the error', () => {
getErrorMessageComponentRegistry().registerValue( getErrorMessageComponentRegistry().registerValue(
'VALID_KEY', 'VALID_KEY',
ERROR_MESSAGE_COMPONENT, ERROR_MESSAGE_COMPONENT,

View File

@@ -30,8 +30,9 @@ import {
waitFor, waitFor,
} from 'spec/helpers/testing-library'; } from 'spec/helpers/testing-library';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries'; import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { Menu } from '@superset-ui/core/components/Menu';
import { supersetGetCache } from 'src/utils/cachedSupersetGet'; import { supersetGetCache } from 'src/utils/cachedSupersetGet';
import { DrillBySubmenu, DrillBySubmenuProps } from './DrillBySubmenu'; import { DrillByMenuItems, DrillByMenuItemsProps } from './DrillByMenuItems';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */ /* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
@@ -78,29 +79,37 @@ const defaultFilters = [
}, },
]; ];
const renderSubmenu = ({ const renderMenu = ({
formData = defaultFormData, formData = defaultFormData,
drillByConfig = { filters: defaultFilters, groupbyFieldName: 'groupby' }, drillByConfig = { filters: defaultFilters, groupbyFieldName: 'groupby' },
dataset = mockDataset, dataset = mockDataset,
...rest ...rest
}: Partial<DrillBySubmenuProps>) => }: Partial<DrillByMenuItemsProps>) =>
render( render(
<DrillBySubmenu <Menu forceSubMenuRender>
formData={formData ?? defaultFormData} <DrillByMenuItems
drillByConfig={drillByConfig} formData={formData ?? defaultFormData}
dataset={dataset} drillByConfig={drillByConfig}
{...rest} dataset={dataset}
/>, open
{...rest}
/>
</Menu>,
{ useRouter: true, useRedux: true }, { useRouter: true, useRedux: true },
); );
const expectDrillByDisabled = async (tooltipContent: string) => { const expectDrillByDisabled = async (tooltipContent: string) => {
const drillByButton = screen.getByRole('button', { name: /drill by/i }); const drillByMenuItem = screen
expect(drillByButton).toBeInTheDocument(); .getAllByRole('menuitem')
expect(drillByButton).toBeVisible(); .find(menuItem => within(menuItem).queryByText('Drill by'));
expect(drillByButton).toHaveAttribute('tabindex', '-1');
const tooltipTrigger = within(drillByButton).getByTestId('tooltip-trigger'); expect(drillByMenuItem).toBeDefined();
expect(drillByMenuItem).toBeVisible();
expect(drillByMenuItem).toHaveAttribute('aria-disabled', 'true');
const tooltipTrigger = within(drillByMenuItem!).getByTestId(
'tooltip-trigger',
);
userEvent.hover(tooltipTrigger as HTMLElement); userEvent.hover(tooltipTrigger as HTMLElement);
const tooltip = await screen.findByRole('tooltip', { name: tooltipContent }); const tooltip = await screen.findByRole('tooltip', { name: tooltipContent });
@@ -108,17 +117,20 @@ const expectDrillByDisabled = async (tooltipContent: string) => {
}; };
const expectDrillByEnabled = async () => { const expectDrillByEnabled = async () => {
const drillByButton = screen.getByRole('button', { name: /drill by/i }); const drillByMenuItem = screen.getByRole('menuitem', {
expect(drillByButton).toBeInTheDocument(); name: 'Drill by',
expect(drillByButton).not.toHaveAttribute('tabindex', '-1'); });
expect(drillByMenuItem).toBeInTheDocument();
const tooltipTrigger = within(drillByButton).queryByTestId('tooltip-trigger'); await waitFor(() =>
expect(drillByMenuItem).not.toHaveAttribute('aria-disabled'),
);
const tooltipTrigger =
within(drillByMenuItem).queryByTestId('tooltip-trigger');
expect(tooltipTrigger).not.toBeInTheDocument(); expect(tooltipTrigger).not.toBeInTheDocument();
userEvent.hover(drillByButton); userEvent.hover(within(drillByMenuItem).getByText('Drill by'));
const drillBySubmenus = await screen.findAllByTestId('drill-by-submenu');
const popover = await screen.findByRole('menu'); expect(drillBySubmenus[0]).toBeInTheDocument();
expect(popover).toBeInTheDocument();
}; };
getChartMetadataRegistry().registerValue( getChartMetadataRegistry().registerValue(
@@ -137,7 +149,7 @@ afterEach(() => {
}); });
test('render disabled menu item for unsupported chart', async () => { test('render disabled menu item for unsupported chart', async () => {
renderSubmenu({ renderMenu({
formData: { ...defaultFormData, viz_type: 'unsupported_viz' }, formData: { ...defaultFormData, viz_type: 'unsupported_viz' },
}); });
await expectDrillByDisabled( await expectDrillByDisabled(
@@ -146,75 +158,89 @@ test('render disabled menu item for unsupported chart', async () => {
}); });
test('render enabled menu item for supported chart, no filters', async () => { test('render enabled menu item for supported chart, no filters', async () => {
renderSubmenu({ renderMenu({ drillByConfig: { filters: [], groupbyFieldName: 'groupby' } });
drillByConfig: { filters: [], groupbyFieldName: 'groupby' },
});
await expectDrillByEnabled(); await expectDrillByEnabled();
}); });
test('render disabled menu item for supported chart, no columns', async () => { test('render disabled menu item for supported chart, no columns', async () => {
const emptyDataset = { ...mockDataset, columns: [], drillable_columns: [] }; const emptyDataset = { ...mockDataset, columns: [], drillable_columns: [] };
renderSubmenu({ dataset: emptyDataset }); renderMenu({ dataset: emptyDataset });
await expectDrillByEnabled(); await expectDrillByEnabled();
screen.getByText('No columns found');
const noColumnsText = await screen.findByText('No columns found');
expect(noColumnsText).toBeInTheDocument();
}); });
test('render menu item with submenu without searchbox', async () => { test('render menu item with submenu without searchbox', async () => {
const slicedColumns = defaultColumns.slice(0, 1); // Use only 1 column to avoid search box const slicedColumns = defaultColumns.slice(0, 9);
const datasetWithSlicedColumns = { const datasetWithSlicedColumns = {
...mockDataset, ...mockDataset,
columns: slicedColumns, columns: slicedColumns,
drillable_columns: slicedColumns, drillable_columns: slicedColumns,
}; };
renderSubmenu({ dataset: datasetWithSlicedColumns }); renderMenu({ dataset: datasetWithSlicedColumns });
await expectDrillByEnabled(); await expectDrillByEnabled();
// Check that the column appears in the popover // Check that each column appears in the drill-by submenu
const col1Element = await screen.findByText('col1'); slicedColumns.forEach(column => {
expect(col1Element).toBeInTheDocument(); const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0]; // Use the first submenu
// Should not have search box for small number of columns expect(within(submenu).getByText(column.column_name)).toBeInTheDocument();
});
expect(screen.queryByRole('textbox')).not.toBeInTheDocument(); expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
}); });
// Add global timeout for all tests
jest.setTimeout(20000);
test('render menu item with submenu and searchbox', async () => { test('render menu item with submenu and searchbox', async () => {
renderSubmenu({ dataset: mockDataset }); renderMenu({ dataset: mockDataset });
await expectDrillByEnabled(); await expectDrillByEnabled();
// Wait for first column to ensure menu is loaded // Wait for all columns to be visible
await screen.findByText('col1'); 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 },
);
// Then check all columns are visible const searchbox = await waitFor(
defaultColumns.forEach(column => { () => screen.getAllByPlaceholderText('Search columns')[0],
expect(screen.getByText(column.column_name)).toBeInTheDocument(); );
});
const searchbox = screen.getByPlaceholderText('Search columns');
expect(searchbox).toBeInTheDocument(); expect(searchbox).toBeInTheDocument();
userEvent.type(searchbox, 'col1'); userEvent.type(searchbox, 'col1');
const expectedFilteredColumnNames = ['col1', 'col10', 'col11']; const expectedFilteredColumnNames = ['col1', 'col10', 'col11'];
// Wait for filtering to take effect by checking for first filtered item // Wait for filtered results
await waitFor(() => { await waitFor(() => {
// Check that non-matching columns are not visible const submenus = screen.getAllByTestId('drill-by-submenu');
expect(screen.queryByText('col2')).not.toBeInTheDocument(); const submenu = submenus[0];
expectedFilteredColumnNames.forEach(colName => {
expect(within(submenu).getByText(colName)).toBeInTheDocument();
});
}); });
// Then verify all expected columns are visible const submenus = screen.getAllByTestId('drill-by-submenu');
expectedFilteredColumnNames.forEach(colName => { const submenu = submenus[0];
expect(screen.getByText(colName)).toBeInTheDocument();
});
// Check that non-matching columns are not visible
defaultColumns defaultColumns
.filter(col => !expectedFilteredColumnNames.includes(col.column_name)) .filter(col => !expectedFilteredColumnNames.includes(col.column_name))
.forEach(col => { .forEach(col => {
expect(screen.queryByText(col.column_name)).not.toBeInTheDocument(); expect(
within(submenu).queryByText(col.column_name),
).not.toBeInTheDocument();
}); });
expectedFilteredColumnNames.forEach(colName => {
expect(within(submenu).getByText(colName)).toBeInTheDocument();
});
}); });
test('Do not display excluded column in the menu', async () => { test('Do not display excluded column in the menu', async () => {
@@ -226,7 +252,7 @@ test('Do not display excluded column in the menu', async () => {
...mockDataset, ...mockDataset,
drillable_columns: filteredColumns, drillable_columns: filteredColumns,
}; };
renderSubmenu({ renderMenu({
dataset: datasetWithFilteredColumns, dataset: datasetWithFilteredColumns,
excludedColumns: excludedColNames.map(colName => ({ excludedColumns: excludedColNames.map(colName => ({
column_name: colName, column_name: colName,
@@ -235,24 +261,32 @@ test('Do not display excluded column in the menu', async () => {
await expectDrillByEnabled(); await expectDrillByEnabled();
// Wait for first column to ensure menu is loaded // Wait for menu items to be loaded
await screen.findByText('col1'); await waitFor(
() => {
// Then check all non-excluded columns are visible const submenus = screen.getAllByTestId('drill-by-submenu');
defaultColumns const submenu = submenus[0];
.filter(column => !excludedColNames.includes(column.column_name)) defaultColumns
.forEach(column => { .filter(column => !excludedColNames.includes(column.column_name))
expect(screen.getByText(column.column_name)).toBeInTheDocument(); .forEach(column => {
}); expect(
within(submenu).getByText(column.column_name),
).toBeInTheDocument();
});
},
{ timeout: 10000 },
);
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
excludedColNames.forEach(colName => { excludedColNames.forEach(colName => {
expect(screen.queryByText(colName)).not.toBeInTheDocument(); expect(within(submenu).queryByText(colName)).not.toBeInTheDocument();
}); });
}); });
test('When menu item is clicked, call onSelection with clicked column and drill by filters', async () => { test('When menu item is clicked, call onSelection with clicked column and drill by filters', async () => {
const onSelectionMock = jest.fn(); const onSelectionMock = jest.fn();
renderSubmenu({ renderMenu({
dataset: mockDataset, dataset: mockDataset,
onSelection: onSelectionMock, onSelection: onSelectionMock,
}); });
@@ -260,7 +294,11 @@ test('When menu item is clicked, call onSelection with clicked column and drill
await expectDrillByEnabled(); await expectDrillByEnabled();
// Wait for col1 to be visible before clicking // Wait for col1 to be visible before clicking
const col1Element = await screen.findByText('col1'); const col1Element = await waitFor(() => {
const submenus = screen.getAllByTestId('drill-by-submenu');
const submenu = submenus[0];
return within(submenu).getByText('col1');
});
userEvent.click(col1Element); userEvent.click(col1Element);
expect(onSelectionMock).toHaveBeenCalledWith( expect(onSelectionMock).toHaveBeenCalledWith(
@@ -271,10 +309,3 @@ test('When menu item is clicked, call onSelection with clicked column and drill
{ filters: defaultFilters, groupbyFieldName: 'groupby' }, { filters: defaultFilters, groupbyFieldName: 'groupby' },
); );
}); });
test('matrixify_enable_vertical_layout should not render component', () => {
const { container } = renderSubmenu({
formData: { ...defaultFormData, matrixify_enable_vertical_layout: true },
});
expect(container).toBeEmptyDOMElement();
});

View File

@@ -0,0 +1,277 @@
/**
* 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,7 +281,6 @@ test('should render "Edit chart" enabled with can_explore permission', async ()
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled(); expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Embedded mode behavior', () => { describe('Embedded mode behavior', () => {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const { isEmbedded } = require('src/dashboard/util/isEmbedded'); const { isEmbedded } = require('src/dashboard/util/isEmbedded');
@@ -358,7 +357,6 @@ describe('Embedded mode behavior', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Table view with pagination', () => { describe('Table view with pagination', () => {
beforeEach(() => { beforeEach(() => {
// Mock a large dataset response for pagination testing // Mock a large dataset response for pagination testing

View File

@@ -1,342 +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 {
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

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

View File

@@ -0,0 +1,253 @@
/**
* 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

@@ -0,0 +1,21 @@
/**
* 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

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

View File

@@ -59,7 +59,6 @@ jest.mock('@superset-ui/core', () => ({
getChartBuildQueryRegistry: jest.fn(), getChartBuildQueryRegistry: jest.fn(),
})); }));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('chart actions', () => { describe('chart actions', () => {
const MOCK_URL = '/mockURL'; const MOCK_URL = '/mockURL';
let dispatch; let dispatch;
@@ -122,13 +121,12 @@ describe('chart actions', () => {
}; };
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('v1 API', () => { describe('v1 API', () => {
beforeEach(() => { beforeEach(() => {
fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false }; fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false };
}); });
test('should query with the built query', async () => { it('should query with the built query', async () => {
const actionThunk = actions.postChartFormData({}, null); const actionThunk = actions.postChartFormData({}, null);
await actionThunk(dispatch, mockGetState); await actionThunk(dispatch, mockGetState);
@@ -143,7 +141,7 @@ describe('chart actions', () => {
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED); expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
}); });
test('should handle the bigint without regression', async () => { it('should handle the bigint without regression', async () => {
getChartDataUriStub.restore(); getChartDataUriStub.restore();
const mockBigIntUrl = '/mock/chart/data/bigint'; const mockBigIntUrl = '/mock/chart/data/bigint';
const expectedBigNumber = '9223372036854775807'; const expectedBigNumber = '9223372036854775807';
@@ -162,7 +160,7 @@ describe('chart actions', () => {
expect(json.value.toString()).toEqual(expectedBigNumber); expect(json.value.toString()).toEqual(expectedBigNumber);
}); });
test('handleChartDataResponse should return result if GlobalAsyncQueries flag is disabled', async () => { it('handleChartDataResponse should return result if GlobalAsyncQueries flag is disabled', async () => {
const result = await handleChartDataResponse( const result = await handleChartDataResponse(
{ status: 200 }, { status: 200 },
{ result: [1, 2, 3] }, { result: [1, 2, 3] },
@@ -170,7 +168,7 @@ describe('chart actions', () => {
expect(result).toEqual([1, 2, 3]); expect(result).toEqual([1, 2, 3]);
}); });
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and results are returned synchronously', async () => { it('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and results are returned synchronously', async () => {
global.featureFlags = { global.featureFlags = {
[FeatureFlag.GlobalAsyncQueries]: true, [FeatureFlag.GlobalAsyncQueries]: true,
}; };
@@ -181,7 +179,7 @@ describe('chart actions', () => {
expect(result).toEqual([1, 2, 3]); expect(result).toEqual([1, 2, 3]);
}); });
test('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and query is running asynchronously', async () => { it('handleChartDataResponse should handle responses when GlobalAsyncQueries flag is enabled and query is running asynchronously', async () => {
global.featureFlags = { global.featureFlags = {
[FeatureFlag.GlobalAsyncQueries]: true, [FeatureFlag.GlobalAsyncQueries]: true,
}; };
@@ -193,13 +191,12 @@ describe('chart actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('legacy API', () => { describe('legacy API', () => {
beforeEach(() => { beforeEach(() => {
fakeMetadata = { useLegacyApi: true }; fakeMetadata = { useLegacyApi: true };
}); });
test('should dispatch CHART_UPDATE_STARTED action before the query', () => { it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
const actionThunk = actions.postChartFormData({}); const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch, mockGetState).then(() => { return actionThunk(dispatch, mockGetState).then(() => {
@@ -210,7 +207,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch TRIGGER_QUERY action with the query', () => { it('should dispatch TRIGGER_QUERY action with the query', () => {
const actionThunk = actions.postChartFormData({}); const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch, mockGetState).then(() => { return actionThunk(dispatch, mockGetState).then(() => {
// chart update, trigger query, update form data, success // chart update, trigger query, update form data, success
@@ -220,7 +217,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => { it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
const actionThunk = actions.postChartFormData({}); const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch, mockGetState).then(() => { return actionThunk(dispatch, mockGetState).then(() => {
// chart update, trigger query, update form data, success // chart update, trigger query, update form data, success
@@ -230,7 +227,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch logEvent async action', () => { it('should dispatch logEvent async action', () => {
const actionThunk = actions.postChartFormData({}); const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch, mockGetState).then(() => { return actionThunk(dispatch, mockGetState).then(() => {
// chart update, trigger query, update form data, success // chart update, trigger query, update form data, success
@@ -244,7 +241,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => { it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
const actionThunk = actions.postChartFormData({}); const actionThunk = actions.postChartFormData({});
return actionThunk(dispatch, mockGetState).then(() => { return actionThunk(dispatch, mockGetState).then(() => {
// chart update, trigger query, update form data, success // chart update, trigger query, update form data, success
@@ -254,7 +251,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => { it('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
const unresolvingPromise = new Promise(() => {}); const unresolvingPromise = new Promise(() => {});
fetchMock.post(MOCK_URL, () => unresolvingPromise, { fetchMock.post(MOCK_URL, () => unresolvingPromise, {
overwriteRoutes: true, overwriteRoutes: true,
@@ -272,7 +269,7 @@ describe('chart actions', () => {
}); });
}); });
test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => { it('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
fetchMock.post( fetchMock.post(
MOCK_URL, MOCK_URL,
{ throws: { statusText: 'misc error' } }, { throws: { statusText: 'misc error' } },
@@ -293,7 +290,7 @@ describe('chart actions', () => {
}); });
}); });
test('should handle the bigint without regression', async () => { it('should handle the bigint without regression', async () => {
getExploreUrlStub.restore(); getExploreUrlStub.restore();
const mockBigIntUrl = '/mock/chart/data/bigint'; const mockBigIntUrl = '/mock/chart/data/bigint';
const expectedBigNumber = '9223372036854775807'; const expectedBigNumber = '9223372036854775807';
@@ -313,14 +310,13 @@ describe('chart actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('runAnnotationQuery', () => { describe('runAnnotationQuery', () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => { it('should dispatch annotationQueryStarted and annotationQuerySuccess on successful query', async () => {
const annotation = { const annotation = {
name: 'Holidays', name: 'Holidays',
annotationType: 'EVENT', annotationType: 'EVENT',
@@ -370,13 +366,12 @@ describe('chart actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('chart actions timeout', () => { describe('chart actions timeout', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('should use the timeout from arguments when given', async () => { it('should use the timeout from arguments when given', async () => {
const postSpy = jest.spyOn(SupersetClient, 'post'); const postSpy = jest.spyOn(SupersetClient, 'post');
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } })); postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
const timeout = 10; // Set the timeout value here const timeout = 10; // Set the timeout value here
@@ -408,7 +403,7 @@ describe('chart actions timeout', () => {
expect(postSpy).toHaveBeenCalledWith(expectedPayload); expect(postSpy).toHaveBeenCalledWith(expectedPayload);
}); });
test('should use the timeout from common.conf when not passed as an argument', async () => { it('should use the timeout from common.conf when not passed as an argument', async () => {
const postSpy = jest.spyOn(SupersetClient, 'post'); const postSpy = jest.spyOn(SupersetClient, 'post');
postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } })); postSpy.mockImplementation(() => Promise.resolve({ json: { result: [] } }));
const formData = { datasource: 'table__1' }; // Set the formData here const formData = { datasource: 'table__1' }; // Set the formData here

View File

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

View File

@@ -73,25 +73,24 @@ beforeEach(() => {
fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] }); fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceModal', () => { describe('DatasourceModal', () => {
test('renders', async () => { it('renders', async () => {
expect(container).toBeDefined(); expect(container).toBeDefined();
}); });
test('renders the component', () => { it('renders the component', () => {
expect(screen.getByText('Edit Dataset')).toBeInTheDocument(); expect(screen.getByText('Edit Dataset')).toBeInTheDocument();
}); });
test('renders a Modal', async () => { it('renders a Modal', async () => {
expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByRole('dialog')).toBeInTheDocument();
}); });
test('renders a DatasourceEditor', async () => { it('renders a DatasourceEditor', async () => {
expect(screen.getByTestId('datasource-editor')).toBeInTheDocument(); expect(screen.getByTestId('datasource-editor')).toBeInTheDocument();
}); });
test('disables the save button when the datasource is managed externally', () => { it('disables the save button when the datasource is managed externally', () => {
// the render is currently in a before operation, so it needs to be cleaned up // the render is currently in a before operation, so it needs to be cleaned up
// we could alternatively move all the renders back into the tests or find a better // we could alternatively move all the renders back into the tests or find a better
// way to automatically render but still allow to pass in props with the tests // way to automatically render but still allow to pass in props with the tests
@@ -105,7 +104,7 @@ describe('DatasourceModal', () => {
expect(saveButton).toBeDisabled(); expect(saveButton).toBeDisabled();
}); });
test('calls the onDatasourceSave function when the save button is clicked', async () => { it('calls the onDatasourceSave function when the save button is clicked', async () => {
cleanup(); cleanup();
const onDatasourceSave = jest.fn(); const onDatasourceSave = jest.fn();
@@ -121,7 +120,7 @@ describe('DatasourceModal', () => {
}); });
}); });
test('should render error dialog', async () => { it('should render error dialog', async () => {
jest jest
.spyOn(SupersetClient, 'put') .spyOn(SupersetClient, 'put')
.mockRejectedValue(new Error('Something went wrong')); .mockRejectedValue(new Error('Something went wrong'));

View File

@@ -1,110 +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,
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,7 +16,13 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { FunctionComponent, useState, useEffect, useCallback } from 'react'; import {
FunctionComponent,
useState,
useRef,
useEffect,
useCallback,
} from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
styled, styled,
@@ -95,7 +101,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [currentDatasource, setCurrentDatasource] = useState(datasource); const [currentDatasource, setCurrentDatasource] = useState(datasource);
const [syncColumns, setSyncColumns] = useState(false); const syncColumnsRef = useRef(false);
const [confirmModal, setConfirmModal] = useState<any>(null);
const currencies = useSelector< const currencies = useSelector<
{ {
common: { common: {
@@ -107,6 +114,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
const [errors, setErrors] = useState<any[]>([]); const [errors, setErrors] = useState<any[]>([]);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
const dialog = useRef<any>(null);
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const buildPayload = (datasource: Record<string, any>) => { const buildPayload = (datasource: Record<string, any>) => {
const payload: Record<string, any> = { const payload: Record<string, any> = {
@@ -188,7 +196,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
setIsSaving(true); setIsSaving(true);
try { try {
await SupersetClient.put({ await SupersetClient.put({
endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${syncColumns}`, endpoint: `/api/v1/dataset/${currentDatasource.id}?override_columns=${syncColumnsRef.current}`,
jsonPayload: buildPayload(currentDatasource), jsonPayload: buildPayload(currentDatasource),
}); });
@@ -273,9 +281,14 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
impact the column definitions, you might want to skip this step.`)} impact the column definitions, you might want to skip this step.`)}
/> />
<Checkbox <Checkbox
checked={syncColumns} checked={syncColumnsRef.current}
onChange={() => { onChange={() => {
setSyncColumns(!syncColumns); syncColumnsRef.current = !syncColumnsRef.current;
if (confirmModal) {
confirmModal.update({
content: getSaveDialog(),
});
}
}} }}
/> />
<span <span
@@ -290,17 +303,25 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
{t('Are you sure you want to save and apply changes?')} {t('Are you sure you want to save and apply changes?')}
</div> </div>
), ),
[currentDatasource.sql, datasource.sql, syncColumns], [currentDatasource.sql, datasource.sql, confirmModal],
); );
useEffect(() => {
if (confirmModal) {
confirmModal.update({
content: getSaveDialog(),
});
}
}, [confirmModal, getSaveDialog]);
useEffect(() => { useEffect(() => {
if (datasource.sql !== currentDatasource.sql) { if (datasource.sql !== currentDatasource.sql) {
setSyncColumns(true); syncColumnsRef.current = true;
} }
}, [datasource.sql, currentDatasource.sql]); }, [datasource.sql, currentDatasource.sql]);
const onClickSave = () => { const onClickSave = () => {
modal.confirm({ const modalInstance = modal.confirm({
title: t('Confirm save'), title: t('Confirm save'),
content: getSaveDialog(), content: getSaveDialog(),
onOk: onConfirmSave, onOk: onConfirmSave,
@@ -308,6 +329,8 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
okText: t('OK'), okText: t('OK'),
cancelText: t('Cancel'), cancelText: t('Cancel'),
}); });
setConfirmModal(modalInstance);
dialog.current = modalInstance;
}; };
return ( return (

View File

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

View File

@@ -740,6 +740,7 @@ class DatasourceEditor extends PureComponent {
this.props.runQuery({ this.props.runQuery({
client_id: this.props.clientId, client_id: this.props.clientId,
database_id: this.state.datasource.database.id, database_id: this.state.datasource.database.id,
json: true,
runAsync: false, runAsync: false,
catalog: this.state.datasource.catalog, catalog: this.state.datasource.catalog,
schema: this.state.datasource.schema, schema: this.state.datasource.schema,

View File

@@ -46,7 +46,6 @@ const setupTest = (dashboards = mockDashboards) =>
}), }),
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DashboardLinksExternal', () => { describe('DashboardLinksExternal', () => {
test('renders empty state when no dashboards provided', () => { test('renders empty state when no dashboards provided', () => {
setupTest([]); setupTest([]);

View File

@@ -63,7 +63,6 @@ export const asyncRender = props =>
}), }),
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceEditor', () => { describe('DatasourceEditor', () => {
beforeAll(() => { beforeAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -81,11 +80,11 @@ describe('DatasourceEditor', () => {
// jest.clearAllMocks(); // jest.clearAllMocks();
}); });
test('renders Tabs', () => { it('renders Tabs', () => {
expect(screen.getByTestId('edit-dataset-tabs')).toBeInTheDocument(); expect(screen.getByTestId('edit-dataset-tabs')).toBeInTheDocument();
}); });
test('can sync columns from source', async () => { it('can sync columns from source', async () => {
const columnsTab = screen.getByTestId('collection-tab-Columns'); const columnsTab = screen.getByTestId('collection-tab-Columns');
userEvent.click(columnsTab); userEvent.click(columnsTab);
@@ -112,7 +111,7 @@ describe('DatasourceEditor', () => {
}); });
// to add, remove and modify columns accordingly // to add, remove and modify columns accordingly
test('can modify columns', async () => { it('can modify columns', async () => {
const columnsTab = screen.getByTestId('collection-tab-Columns'); const columnsTab = screen.getByTestId('collection-tab-Columns');
userEvent.click(columnsTab); userEvent.click(columnsTab);
@@ -139,7 +138,7 @@ describe('DatasourceEditor', () => {
userEvent.type(inputCertDetails, 'test'); userEvent.type(inputCertDetails, 'test');
}, 40000); }, 40000);
test('can delete columns', async () => { it('can delete columns', async () => {
const columnsTab = screen.getByTestId('collection-tab-Columns'); const columnsTab = screen.getByTestId('collection-tab-Columns');
userEvent.click(columnsTab); userEvent.click(columnsTab);
@@ -163,7 +162,7 @@ describe('DatasourceEditor', () => {
}); });
}, 60000); // 60 seconds timeout to avoid timeouts }, 60000); // 60 seconds timeout to avoid timeouts
test('can add new columns', async () => { it('can add new columns', async () => {
const calcColsTab = screen.getByTestId('collection-tab-Calculated columns'); const calcColsTab = screen.getByTestId('collection-tab-Calculated columns');
userEvent.click(calcColsTab); userEvent.click(calcColsTab);
@@ -181,7 +180,7 @@ describe('DatasourceEditor', () => {
}); });
}, 60000); }, 60000);
test('renders isSqla fields', async () => { it('renders isSqla fields', async () => {
const columnsTab = screen.getByRole('tab', { const columnsTab = screen.getByRole('tab', {
name: /settings/i, name: /settings/i,
}); });
@@ -196,7 +195,6 @@ describe('DatasourceEditor', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DatasourceEditor Source Tab', () => { describe('DatasourceEditor Source Tab', () => {
beforeAll(() => { beforeAll(() => {
isFeatureEnabled.mockImplementation(() => false); isFeatureEnabled.mockImplementation(() => false);
@@ -218,7 +216,7 @@ describe('DatasourceEditor Source Tab', () => {
isFeatureEnabled.mockRestore(); isFeatureEnabled.mockRestore();
}); });
test('Source Tab: edit mode', async () => { it('Source Tab: edit mode', async () => {
const getLockBtn = screen.getByRole('img', { name: /lock/i }); const getLockBtn = screen.getByRole('img', { name: /lock/i });
userEvent.click(getLockBtn); userEvent.click(getLockBtn);
@@ -233,7 +231,7 @@ describe('DatasourceEditor Source Tab', () => {
expect(virtualRadioBtn).toBeEnabled(); expect(virtualRadioBtn).toBeEnabled();
}); });
test('Source Tab: readOnly mode', () => { it('Source Tab: readOnly mode', () => {
const getLockBtn = screen.getByRole('img', { name: /lock/i }); const getLockBtn = screen.getByRole('img', { name: /lock/i });
expect(getLockBtn).toBeInTheDocument(); expect(getLockBtn).toBeInTheDocument();
@@ -248,7 +246,7 @@ describe('DatasourceEditor Source Tab', () => {
expect(virtualRadioBtn).toBeDisabled(); expect(virtualRadioBtn).toBeDisabled();
}); });
test('calls onChange with empty SQL when switching to physical dataset', async () => { it('calls onChange with empty SQL when switching to physical dataset', async () => {
// Clean previous render // Clean previous render
cleanup(); cleanup();

View File

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

View File

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

View File

@@ -20,7 +20,6 @@
import { tn } from '@superset-ui/core'; import { tn } from '@superset-ui/core';
import { updateColumns } from '.'; import { updateColumns } from '.';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('updateColumns', () => { describe('updateColumns', () => {
let addSuccessToast: jest.Mock; let addSuccessToast: jest.Mock;
@@ -28,7 +27,7 @@ describe('updateColumns', () => {
addSuccessToast = jest.fn(); addSuccessToast = jest.fn();
}); });
test('adds new columns when prevCols is empty', () => { it('adds new columns when prevCols is empty', () => {
interface Column { interface Column {
column_name: string; column_name: string;
type: string; type: string;
@@ -57,7 +56,7 @@ describe('updateColumns', () => {
); );
}); });
test('modifies columns when type or is_dttm changes', () => { it('modifies columns when type or is_dttm changes', () => {
const prevCols = [ const prevCols = [
{ column_name: 'col1', type: 'string', is_dttm: false }, { column_name: 'col1', type: 'string', is_dttm: false },
{ column_name: 'col2', type: 'number', is_dttm: false }, { column_name: 'col2', type: 'number', is_dttm: false },
@@ -95,7 +94,7 @@ describe('updateColumns', () => {
); );
}); });
test('removes columns not present in newCols', () => { it('removes columns not present in newCols', () => {
const prevCols = [ const prevCols = [
{ column_name: 'col1', type: 'string', is_dttm: false }, { column_name: 'col1', type: 'string', is_dttm: false },
{ column_name: 'col2', type: 'number', is_dttm: true }, { column_name: 'col2', type: 'number', is_dttm: true },
@@ -122,7 +121,7 @@ describe('updateColumns', () => {
); );
}); });
test('handles combined additions, modifications, and removals', () => { it('handles combined additions, modifications, and removals', () => {
const prevCols = [ const prevCols = [
{ column_name: 'col1', type: 'string', is_dttm: false }, { column_name: 'col1', type: 'string', is_dttm: false },
{ column_name: 'col2', type: 'number', is_dttm: false }, { column_name: 'col2', type: 'number', is_dttm: false },
@@ -171,7 +170,7 @@ describe('updateColumns', () => {
], ],
]); ]);
}); });
test('should not remove columns with expressions', () => { it('should not remove columns with expressions', () => {
const prevCols = [ const prevCols = [
{ column_name: 'col1', type: 'string', is_dttm: false }, { column_name: 'col1', type: 'string', is_dttm: false },
{ column_name: 'col2', type: 'number', is_dttm: false }, { column_name: 'col2', type: 'number', is_dttm: false },

View File

@@ -20,71 +20,72 @@
import { screen, fireEvent, render } from 'spec/helpers/testing-library'; import { screen, fireEvent, render } from 'spec/helpers/testing-library';
import { ErrorAlert } from './ErrorAlert'; import { ErrorAlert } from './ErrorAlert';
// ErrorAlert describe('ErrorAlert', () => {
test('ErrorAlert renders the error message correctly', () => { it('renders the error message correctly', () => {
render( render(
<ErrorAlert <ErrorAlert
errorType="Error" errorType="Error"
message="Something went wrong" message="Something went wrong"
type="error" type="error"
/>, />,
); );
expect(screen.getByText('Error')).toBeInTheDocument(); expect(screen.getByText('Error')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument(); expect(screen.getByText('Something went wrong')).toBeInTheDocument();
}); });
test('ErrorAlert renders the description when provided', () => { it('renders the description when provided', () => {
const description = 'This is a detailed description'; const description = 'This is a detailed description';
render( render(
<ErrorAlert <ErrorAlert
errorType="Error" errorType="Error"
message="Something went wrong" message="Something went wrong"
type="error" type="error"
description={description} description={description}
/>, />,
); );
expect(screen.getByText(description)).toBeInTheDocument(); expect(screen.getByText(description)).toBeInTheDocument();
}); });
test('ErrorAlert toggles description details visibility when show more/less is clicked', () => { it('toggles description details visibility when show more/less is clicked', () => {
const descriptionDetails = 'Additional details about the error.'; const descriptionDetails = 'Additional details about the error.';
render( render(
<ErrorAlert <ErrorAlert
errorType="Error" errorType="Error"
message="Something went wrong" message="Something went wrong"
type="error" type="error"
descriptionDetails={descriptionDetails} descriptionDetails={descriptionDetails}
descriptionDetailsCollapsed descriptionDetailsCollapsed
/>, />,
); );
const showMoreButton = screen.getByText('See more'); const showMoreButton = screen.getByText('See more');
expect(showMoreButton).toBeInTheDocument(); expect(showMoreButton).toBeInTheDocument();
fireEvent.click(showMoreButton); fireEvent.click(showMoreButton);
expect(screen.getByText(descriptionDetails)).toBeInTheDocument(); expect(screen.getByText(descriptionDetails)).toBeInTheDocument();
const showLessButton = screen.getByText('See less'); const showLessButton = screen.getByText('See less');
fireEvent.click(showLessButton); fireEvent.click(showLessButton);
expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument(); expect(screen.queryByText(descriptionDetails)).not.toBeInTheDocument();
}); });
test('ErrorAlert renders compact mode with a tooltip and modal', () => { it('renders compact mode with a tooltip and modal', () => {
render( render(
<ErrorAlert <ErrorAlert
errorType="Error" errorType="Error"
message="Compact mode example" message="Compact mode example"
type="error" type="error"
compact compact
descriptionDetails="Detailed description in compact mode." descriptionDetails="Detailed description in compact mode."
/>, />,
); );
const iconTrigger = screen.getByText('Error'); const iconTrigger = screen.getByText('Error');
expect(iconTrigger).toBeInTheDocument(); expect(iconTrigger).toBeInTheDocument();
fireEvent.click(iconTrigger); fireEvent.click(iconTrigger);
expect(screen.getByText('Compact mode example')).toBeInTheDocument(); expect(screen.getByText('Compact mode example')).toBeInTheDocument();
});
}); });

View File

@@ -54,7 +54,6 @@ const missingExtraProps = {
const renderComponent = (overrides = {}) => const renderComponent = (overrides = {}) =>
render(<InvalidSQLErrorMessage {...defaultProps} {...overrides} />); render(<InvalidSQLErrorMessage {...defaultProps} {...overrides} />);
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('InvalidSQLErrorMessage', () => { describe('InvalidSQLErrorMessage', () => {
beforeAll(() => { beforeAll(() => {
jest.setTimeout(30000); jest.setTimeout(30000);
@@ -65,7 +64,7 @@ describe('InvalidSQLErrorMessage', () => {
await new Promise(resolve => setTimeout(resolve, 0)); await new Promise(resolve => setTimeout(resolve, 0));
}); });
test('renders the error message with correct properties', async () => { it('renders the error message with correct properties', async () => {
const { getByText, unmount } = renderComponent(); const { getByText, unmount } = renderComponent();
// Validate main properties // Validate main properties
@@ -76,13 +75,13 @@ describe('InvalidSQLErrorMessage', () => {
unmount(); unmount();
}); });
test('renders the error message with the empty extra properties', () => { it('renders the error message with the empty extra properties', () => {
const { getByText } = renderComponent(missingExtraProps); const { getByText } = renderComponent(missingExtraProps);
expect(getByText('Unable to parse SQL')).toBeInTheDocument(); expect(getByText('Unable to parse SQL')).toBeInTheDocument();
expect(getByText(missingExtraProps.error.message)).toBeInTheDocument(); expect(getByText(missingExtraProps.error.message)).toBeInTheDocument();
}); });
test('displays the SQL error line and column indicator', async () => { it('displays the SQL error line and column indicator', async () => {
const { getByText, container, unmount } = renderComponent(); const { getByText, container, unmount } = renderComponent();
// Validate SQL and caret indicator // Validate SQL and caret indicator
@@ -96,7 +95,7 @@ describe('InvalidSQLErrorMessage', () => {
unmount(); unmount();
}); });
test('handles missing line number gracefully', async () => { it('handles missing line number gracefully', async () => {
const overrides = { const overrides = {
error: { error: {
...defaultProps.error, ...defaultProps.error,
@@ -115,7 +114,7 @@ describe('InvalidSQLErrorMessage', () => {
unmount(); unmount();
}); });
test('handles missing column number gracefully', async () => { it('handles missing column number gracefully', async () => {
const overrides = { const overrides = {
error: { error: {
...defaultProps.error, ...defaultProps.error,

View File

@@ -21,7 +21,6 @@ import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import { ErrorLevel, ErrorTypeEnum } from '@superset-ui/core'; import { ErrorLevel, ErrorTypeEnum } from '@superset-ui/core';
import { MarshmallowErrorMessage } from './MarshmallowErrorMessage'; import { MarshmallowErrorMessage } from './MarshmallowErrorMessage';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('MarshmallowErrorMessage', () => { describe('MarshmallowErrorMessage', () => {
const mockError = { const mockError = {
extra: { extra: {

View File

@@ -99,16 +99,15 @@ const setup = (overrides = {}) => (
</Provider> </Provider>
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('OAuth2RedirectMessage Component', () => { describe('OAuth2RedirectMessage Component', () => {
test('renders without crashing and displays the correct initial UI elements', () => { it('renders without crashing and displays the correct initial UI elements', () => {
const { getByText } = render(setup()); const { getByText } = render(setup());
expect(getByText(/Authorization needed/i)).toBeInTheDocument(); expect(getByText(/Authorization needed/i)).toBeInTheDocument();
expect(getByText(/provide authorization/i)).toBeInTheDocument(); expect(getByText(/provide authorization/i)).toBeInTheDocument();
}); });
test('opens a new window with the correct URL when the link is clicked', () => { it('opens a new window with the correct URL when the link is clicked', () => {
const { getByText } = render(setup()); const { getByText } = render(setup());
const linkElement = getByText(/provide authorization/i); const linkElement = getByText(/provide authorization/i);
@@ -117,7 +116,7 @@ describe('OAuth2RedirectMessage Component', () => {
expect(mockOpen).toHaveBeenCalledWith('https://example.com', '_blank'); expect(mockOpen).toHaveBeenCalledWith('https://example.com', '_blank');
}); });
test('cleans up the message event listener on unmount', () => { it('cleans up the message event listener on unmount', () => {
const { unmount } = render(setup()); const { unmount } = render(setup());
expect(mockAddEventListener).toHaveBeenCalled(); expect(mockAddEventListener).toHaveBeenCalled();
@@ -125,7 +124,7 @@ describe('OAuth2RedirectMessage Component', () => {
expect(mockRemoveEventListener).toHaveBeenCalled(); expect(mockRemoveEventListener).toHaveBeenCalled();
}); });
test('dispatches reRunQuery action when a message with correct tab ID is received for SQL Lab', async () => { it('dispatches reRunQuery action when a message with correct tab ID is received for SQL Lab', async () => {
render(setup()); render(setup());
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com'); simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
@@ -135,7 +134,7 @@ describe('OAuth2RedirectMessage Component', () => {
}); });
}); });
test('dispatches triggerQuery action for explore source upon receiving a correct message', async () => { it('dispatches triggerQuery action for explore source upon receiving a correct message', async () => {
render(setup({ source: 'explore' })); render(setup({ source: 'explore' }));
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com'); simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');
@@ -145,7 +144,7 @@ describe('OAuth2RedirectMessage Component', () => {
}); });
}); });
test('dispatches onRefresh action for dashboard source upon receiving a correct message', async () => { it('dispatches onRefresh action for dashboard source upon receiving a correct message', async () => {
render(setup({ source: 'dashboard' })); render(setup({ source: 'dashboard' }));
simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com'); simulateMessageEvent({ tabId: 'tabId' }, 'https://redirect.example.com');

View File

@@ -57,16 +57,15 @@ afterEach(() => {
cleanup(); cleanup();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FacePile', () => { describe('FacePile', () => {
test('renders empty state with no users', () => { it('renders empty state with no users', () => {
const { container } = render(<FacePile users={[]} />, { store }); const { container } = render(<FacePile users={[]} />, { store });
expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument(); expect(container.querySelector('.ant-avatar-group')).toBeInTheDocument();
expect(container.querySelectorAll('.ant-avatar')).toHaveLength(0); expect(container.querySelectorAll('.ant-avatar')).toHaveLength(0);
}); });
test('renders single user without truncation', () => { it('renders single user without truncation', () => {
const { container } = render(<FacePile users={users.slice(0, 1)} />, { const { container } = render(<FacePile users={users.slice(0, 1)} />, {
store, store,
}); });
@@ -77,7 +76,7 @@ describe('FacePile', () => {
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument(); expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
}); });
test('renders multiple users no truncation', () => { it('renders multiple users no truncation', () => {
const { container } = render(<FacePile users={users.slice(0, 4)} />, { const { container } = render(<FacePile users={users.slice(0, 4)} />, {
store, store,
}); });
@@ -91,7 +90,7 @@ describe('FacePile', () => {
expect(within(container).queryByText(/\+/)).not.toBeInTheDocument(); expect(within(container).queryByText(/\+/)).not.toBeInTheDocument();
}); });
test('renders multiple users with truncation', () => { it('renders multiple users with truncation', () => {
const { container } = render(<FacePile users={users} />, { store }); const { container } = render(<FacePile users={users} />, { store });
// Should show 4 avatars + 1 overflow indicator = 5 total elements // Should show 4 avatars + 1 overflow indicator = 5 total elements
@@ -108,7 +107,7 @@ describe('FacePile', () => {
expect(within(container).getByText('+6')).toBeInTheDocument(); expect(within(container).getByText('+6')).toBeInTheDocument();
}); });
test('displays user tooltip on hover', () => { it('displays user tooltip on hover', () => {
const { container } = render(<FacePile users={users.slice(0, 2)} />, { const { container } = render(<FacePile users={users.slice(0, 2)} />, {
store, store,
}); });
@@ -120,7 +119,7 @@ describe('FacePile', () => {
expect(screen.getByRole('tooltip')).toHaveTextContent('user 0'); expect(screen.getByRole('tooltip')).toHaveTextContent('user 0');
}); });
test('displays avatar images when Slack avatars are enabled', () => { it('displays avatar images when Slack avatars are enabled', () => {
// Enable Slack avatars feature flag // Enable Slack avatars feature flag
mockIsFeatureEnabled.mockImplementation( mockIsFeatureEnabled.mockImplementation(
feature => feature === 'SLACK_ENABLE_AVATARS', feature => feature === 'SLACK_ENABLE_AVATARS',
@@ -144,26 +143,24 @@ describe('FacePile', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('utils', () => { describe('utils', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('getRandomColor', () => { describe('getRandomColor', () => {
const colors = ['color1', 'color2', 'color3']; const colors = ['color1', 'color2', 'color3'];
test('produces the same color for the same input values', () => { it('produces the same color for the same input values', () => {
const name = 'foo'; const name = 'foo';
expect(getRandomColor(name, colors)).toEqual( expect(getRandomColor(name, colors)).toEqual(
getRandomColor(name, colors), getRandomColor(name, colors),
); );
}); });
test('produces a different color for different input values', () => { it('produces a different color for different input values', () => {
expect(getRandomColor('foo', colors)).not.toEqual( expect(getRandomColor('foo', colors)).not.toEqual(
getRandomColor('bar', colors), getRandomColor('bar', colors),
); );
}); });
test('handles non-ascii input values', () => { it('handles non-ascii input values', () => {
expect(getRandomColor('泰', colors)).toMatchInlineSnapshot(`"color1"`); expect(getRandomColor('泰', colors)).toMatchInlineSnapshot(`"color1"`);
expect(getRandomColor('مُحَمَّد‎', colors)).toMatchInlineSnapshot( expect(getRandomColor('مُحَمَّد‎', colors)).toMatchInlineSnapshot(
`"color2"`, `"color2"`,

View File

@@ -26,7 +26,6 @@ import {
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact'; import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import { FilterableTable } from '.'; import { FilterableTable } from '.';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FilterableTable', () => { describe('FilterableTable', () => {
beforeAll(() => { beforeAll(() => {
setupAGGridModules(); setupAGGridModules();
@@ -41,10 +40,10 @@ describe('FilterableTable', () => {
], ],
height: 500, height: 500,
}; };
test('is valid element', () => { it('is valid element', () => {
expect(isValidElement(<FilterableTable {...mockedProps} />)).toBe(true); expect(isValidElement(<FilterableTable {...mockedProps} />)).toBe(true);
}); });
test('renders a grid with 3 Table rows', () => { it('renders a grid with 3 Table rows', () => {
const { getByRole, getByText } = render( const { getByRole, getByText } = render(
<FilterableTable {...mockedProps} />, <FilterableTable {...mockedProps} />,
); );
@@ -53,7 +52,7 @@ describe('FilterableTable', () => {
expect(getByText(columnBContent)).toBeInTheDocument(); expect(getByText(columnBContent)).toBeInTheDocument();
}); });
}); });
test('filters on a string', () => { it('filters on a string', () => {
const props = { const props = {
...mockedProps, ...mockedProps,
filterText: 'b1', filterText: 'b1',
@@ -63,7 +62,7 @@ describe('FilterableTable', () => {
expect(queryByText('b2')).not.toBeInTheDocument(); expect(queryByText('b2')).not.toBeInTheDocument();
expect(queryByText('b3')).not.toBeInTheDocument(); expect(queryByText('b3')).not.toBeInTheDocument();
}); });
test('filters on a number', () => { it('filters on a number', () => {
const props = { const props = {
...mockedProps, ...mockedProps,
filterText: '100', filterText: '100',
@@ -75,13 +74,12 @@ describe('FilterableTable', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FilterableTable sorting - RTL', () => { describe('FilterableTable sorting - RTL', () => {
beforeAll(() => { beforeAll(() => {
setupAGGridModules(); setupAGGridModules();
}); });
test('sorts strings correctly', () => { it('sorts strings correctly', () => {
const stringProps = { const stringProps = {
orderedColumnKeys: ['columnA'], orderedColumnKeys: ['columnA'],
data: [ data: [
@@ -130,7 +128,7 @@ describe('FilterableTable sorting - RTL', () => {
); );
}); });
test('sorts integers correctly', () => { it('sorts integers correctly', () => {
const integerProps = { const integerProps = {
orderedColumnKeys: ['columnB'], orderedColumnKeys: ['columnB'],
data: [{ columnB: 21 }, { columnB: 0 }, { columnB: 623 }], data: [{ columnB: 21 }, { columnB: 0 }, { columnB: 623 }],
@@ -165,7 +163,7 @@ describe('FilterableTable sorting - RTL', () => {
expect(gridCells?.textContent).toEqual(['21', '0', '623'].join('')); expect(gridCells?.textContent).toEqual(['21', '0', '623'].join(''));
}); });
test('sorts floating numbers correctly', () => { it('sorts floating numbers correctly', () => {
const floatProps = { const floatProps = {
orderedColumnKeys: ['columnC'], orderedColumnKeys: ['columnC'],
data: [{ columnC: 45.67 }, { columnC: 1.23 }, { columnC: 89.0000001 }], data: [{ columnC: 45.67 }, { columnC: 1.23 }, { columnC: 89.0000001 }],
@@ -208,7 +206,7 @@ describe('FilterableTable sorting - RTL', () => {
); );
}); });
test('sorts rows properly when floating numbers have mixed types', () => { it('sorts rows properly when floating numbers have mixed types', () => {
const mixedFloatProps = { const mixedFloatProps = {
orderedColumnKeys: ['columnD'], orderedColumnKeys: ['columnD'],
data: [ data: [
@@ -310,7 +308,7 @@ describe('FilterableTable sorting - RTL', () => {
); );
}); });
test('sorts YYYY-MM-DD properly', () => { it('sorts YYYY-MM-DD properly', () => {
const dsProps = { const dsProps = {
orderedColumnKeys: ['columnDS'], orderedColumnKeys: ['columnDS'],
data: [ data: [

View File

@@ -0,0 +1,65 @@
/**
* 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

@@ -0,0 +1,51 @@
/**
* 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

@@ -0,0 +1,20 @@
/**
* 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,7 +152,6 @@ test('renders unhide when invisible column exists', async () => {
expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith(['column2'], true); expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith(['column2'], true);
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('for main menu', () => { describe('for main menu', () => {
test('renders Copy to Clipboard', async () => { test('renders Copy to Clipboard', async () => {
const { getByText } = setup({ ...mockedProps, isMain: true }); const { getByText } = setup({ ...mockedProps, isMain: true });

View File

@@ -130,7 +130,6 @@ const factory = (props = mockedProps) =>
{ store: mockStore() }, { store: mockStore() },
); );
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('ListView', () => { describe('ListView', () => {
beforeEach(() => { beforeEach(() => {
fetchMock.reset(); fetchMock.reset();
@@ -147,7 +146,7 @@ describe('ListView', () => {
}); });
// Example of converted test: // Example of converted test:
test('calls fetchData on mount', () => { it('calls fetchData on mount', () => {
expect(mockedProps.fetchData).toHaveBeenCalledWith({ expect(mockedProps.fetchData).toHaveBeenCalledWith({
filters: [], filters: [],
pageIndex: 0, pageIndex: 0,
@@ -156,7 +155,7 @@ describe('ListView', () => {
}); });
}); });
test('calls fetchData on sort', async () => { it('calls fetchData on sort', async () => {
const sortHeader = screen.getAllByTestId('sort-header')[1]; const sortHeader = screen.getAllByTestId('sort-header')[1];
await userEvent.click(sortHeader); await userEvent.click(sortHeader);
@@ -174,7 +173,7 @@ describe('ListView', () => {
}); });
// Update pagination control tests for Ant Design pagination // Update pagination control tests for Ant Design pagination
test('renders pagination controls', () => { it('renders pagination controls', () => {
const paginationList = screen.getByRole('list'); const paginationList = screen.getByRole('list');
expect(paginationList).toBeInTheDocument(); expect(paginationList).toBeInTheDocument();
@@ -182,7 +181,7 @@ describe('ListView', () => {
expect(pageOneItem).toBeInTheDocument(); expect(pageOneItem).toBeInTheDocument();
}); });
test('calls fetchData on page change', async () => { it('calls fetchData on page change', async () => {
const pageTwoItem = screen.getByRole('listitem', { name: '2' }); const pageTwoItem = screen.getByRole('listitem', { name: '2' });
await userEvent.click(pageTwoItem); await userEvent.click(pageTwoItem);
@@ -198,7 +197,7 @@ describe('ListView', () => {
}); });
}); });
test('handles bulk actions on 1 row', async () => { it('handles bulk actions on 1 row', async () => {
const checkboxes = screen.getAllByRole('checkbox'); const checkboxes = screen.getAllByRole('checkbox');
await userEvent.click(checkboxes[1]); // Index 1 is the first row checkbox await userEvent.click(checkboxes[1]); // Index 1 is the first row checkbox
@@ -218,12 +217,12 @@ describe('ListView', () => {
}); });
// Update UI filters test to use more specific selector // Update UI filters test to use more specific selector
test('renders UI filters', () => { it('renders UI filters', () => {
const filterControls = screen.getAllByRole('combobox'); const filterControls = screen.getAllByRole('combobox');
expect(filterControls).toHaveLength(2); expect(filterControls).toHaveLength(2);
}); });
test('calls fetchData on filter', async () => { it('calls fetchData on filter', async () => {
// Handle select filter // Handle select filter
const selectFilter = screen.getAllByRole('combobox')[0]; const selectFilter = screen.getAllByRole('combobox')[0];
await userEvent.click(selectFilter); await userEvent.click(selectFilter);
@@ -253,7 +252,7 @@ describe('ListView', () => {
); );
}); });
test('calls fetchData on card view sort', async () => { it('calls fetchData on card view sort', async () => {
factory({ factory({
...mockedProps, ...mockedProps,
renderCard: jest.fn(), renderCard: jest.fn(),

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,6 @@ import useStoredSidebarWidth from './useStoredSidebarWidth';
const INITIAL_WIDTH = 300; const INITIAL_WIDTH = 300;
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('useStoredSidebarWidth', () => { describe('useStoredSidebarWidth', () => {
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
@@ -36,7 +35,7 @@ describe('useStoredSidebarWidth', () => {
localStorage.clear(); localStorage.clear();
}); });
test('returns a default filterBar width by initialWidth', () => { it('returns a default filterBar width by initialWidth', () => {
const id = '123'; const id = '123';
const { result } = renderHook(() => const { result } = renderHook(() =>
useStoredSidebarWidth(id, INITIAL_WIDTH), useStoredSidebarWidth(id, INITIAL_WIDTH),
@@ -46,7 +45,7 @@ describe('useStoredSidebarWidth', () => {
expect(actualWidth).toEqual(INITIAL_WIDTH); expect(actualWidth).toEqual(INITIAL_WIDTH);
}); });
test('returns a stored filterBar width from localStorage', () => { it('returns a stored filterBar width from localStorage', () => {
const id = '123'; const id = '123';
const expectedWidth = 378; const expectedWidth = 378;
setItem(LocalStorageKeys.CommonResizableSidebarWidths, { setItem(LocalStorageKeys.CommonResizableSidebarWidths, {
@@ -62,7 +61,7 @@ describe('useStoredSidebarWidth', () => {
expect(actualWidth).not.toEqual(250); expect(actualWidth).not.toEqual(250);
}); });
test('returns a setter for filterBar width that stores the state in localStorage together', () => { it('returns a setter for filterBar width that stores the state in localStorage together', () => {
const id = '123'; const id = '123';
const expectedWidth = 378; const expectedWidth = 378;
const otherDashboardId = '456'; const otherDashboardId = '456';

View File

@@ -41,13 +41,12 @@ const defaultProps = {
datasourceType: 'table', datasourceType: 'table',
}; };
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('SQLEditorWithValidation', () => { describe('SQLEditorWithValidation', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('renders SQLEditor with validation bar when showValidation is true', () => { it('renders SQLEditor with validation bar when showValidation is true', () => {
render(<SQLEditorWithValidation {...defaultProps} />); render(<SQLEditorWithValidation {...defaultProps} />);
expect(screen.getByText('Unverified')).toBeInTheDocument(); expect(screen.getByText('Unverified')).toBeInTheDocument();
@@ -56,7 +55,7 @@ describe('SQLEditorWithValidation', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
test('does not render validation bar when showValidation is false', () => { it('does not render validation bar when showValidation is false', () => {
render( render(
<SQLEditorWithValidation {...defaultProps} showValidation={false} />, <SQLEditorWithValidation {...defaultProps} showValidation={false} />,
); );
@@ -67,7 +66,7 @@ describe('SQLEditorWithValidation', () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
test('shows primary button style when unverified', () => { it('shows primary button style when unverified', () => {
render(<SQLEditorWithValidation {...defaultProps} />); render(<SQLEditorWithValidation {...defaultProps} />);
const validateButton = screen.getByRole('button', { const validateButton = screen.getByRole('button', {
@@ -77,7 +76,7 @@ describe('SQLEditorWithValidation', () => {
// Button should have primary styling (this would need to check actual class or style) // Button should have primary styling (this would need to check actual class or style)
}); });
test('disables validate button when no value or datasourceId', () => { it('disables validate button when no value or datasourceId', () => {
render( render(
<SQLEditorWithValidation <SQLEditorWithValidation
{...defaultProps} {...defaultProps}
@@ -92,7 +91,7 @@ describe('SQLEditorWithValidation', () => {
expect(validateButton).toBeDisabled(); expect(validateButton).toBeDisabled();
}); });
test('shows validating state when validation is in progress', async () => { it('shows validating state when validation is in progress', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -118,7 +117,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('shows success state when validation passes', async () => { it('shows success state when validation passes', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -139,7 +138,7 @@ describe('SQLEditorWithValidation', () => {
expect(validateButton).toBeInTheDocument(); expect(validateButton).toBeInTheDocument();
}); });
test('shows error state when validation fails', async () => { it('shows error state when validation fails', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -170,7 +169,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('handles API errors gracefully', async () => { it('handles API errors gracefully', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -190,7 +189,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('sends correct payload for column expression', async () => { it('sends correct payload for column expression', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -222,7 +221,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('sends correct payload for WHERE expression', async () => { it('sends correct payload for WHERE expression', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -253,7 +252,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('sends correct payload for HAVING expression', async () => { it('sends correct payload for HAVING expression', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;
@@ -284,7 +283,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('resets validation state when value changes', () => { it('resets validation state when value changes', () => {
const { rerender } = render(<SQLEditorWithValidation {...defaultProps} />); const { rerender } = render(<SQLEditorWithValidation {...defaultProps} />);
// Simulate having a validation result // Simulate having a validation result
@@ -305,7 +304,7 @@ describe('SQLEditorWithValidation', () => {
expect(screen.getByText('Unverified')).toBeInTheDocument(); expect(screen.getByText('Unverified')).toBeInTheDocument();
}); });
test('calls onChange when editor value changes', () => { it('calls onChange when editor value changes', () => {
const onChange = jest.fn(); const onChange = jest.fn();
render(<SQLEditorWithValidation {...defaultProps} onChange={onChange} />); render(<SQLEditorWithValidation {...defaultProps} onChange={onChange} />);
@@ -314,7 +313,7 @@ describe('SQLEditorWithValidation', () => {
expect(onChange).toBeDefined(); expect(onChange).toBeDefined();
}); });
test('calls onValidationComplete callback when provided', async () => { it('calls onValidationComplete callback when provided', async () => {
const onValidationComplete = jest.fn(); const onValidationComplete = jest.fn();
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
@@ -338,7 +337,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('calls onValidationComplete with errors when validation fails', async () => { it('calls onValidationComplete with errors when validation fails', async () => {
const onValidationComplete = jest.fn(); const onValidationComplete = jest.fn();
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
@@ -372,7 +371,7 @@ describe('SQLEditorWithValidation', () => {
}); });
}); });
test('shows tooltip with full error message when error is truncated', async () => { it('shows tooltip with full error message when error is truncated', async () => {
const longErrorMessage = const longErrorMessage =
'This is a very long error message that should be truncated in the display but shown in full in the tooltip when user hovers over it'; 'This is a very long error message that should be truncated in the display but shown in full in the tooltip when user hovers over it';
@@ -411,7 +410,7 @@ describe('SQLEditorWithValidation', () => {
expect(errorElement.parentElement).toBeTruthy(); expect(errorElement.parentElement).toBeTruthy();
}); });
test('handles empty response gracefully', async () => { it('handles empty response gracefully', async () => {
const mockPost = SupersetClient.post as jest.MockedFunction< const mockPost = SupersetClient.post as jest.MockedFunction<
typeof SupersetClient.post typeof SupersetClient.post
>; >;

View File

@@ -20,7 +20,6 @@ import fetchMock from 'fetch-mock';
import rison from 'rison'; import rison from 'rison';
import { tagToSelectOption, loadTags } from 'src/components/Tag/utils'; import { tagToSelectOption, loadTags } from 'src/components/Tag/utils';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('tagToSelectOption', () => { describe('tagToSelectOption', () => {
test('converts a Tag object with table_name to a SelectTagsValue', () => { test('converts a Tag object with table_name to a SelectTagsValue', () => {
const tag = { const tag = {
@@ -39,7 +38,6 @@ describe('tagToSelectOption', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('loadTags', () => { describe('loadTags', () => {
beforeEach(() => { beforeEach(() => {
fetchMock.reset(); fetchMock.reset();

View File

@@ -79,7 +79,6 @@ test('should render 3 elements when maxTags is set to 3', async () => {
expect(tagsListItems[2]).toHaveTextContent('+3...'); expect(tagsListItems[2]).toHaveTextContent('+3...');
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Tag type filtering', () => { describe('Tag type filtering', () => {
test('should render only custom type tags (type: 1)', async () => { test('should render only custom type tags (type: 1)', async () => {
const mixedTypeTags = [ const mixedTypeTags = [

View File

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

View File

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

View File

@@ -58,7 +58,6 @@ import {
NEW_ROW_ID, NEW_ROW_ID,
} from 'src/dashboard/util/constants'; } from 'src/dashboard/util/constants';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dashboardLayout actions', () => { describe('dashboardLayout actions', () => {
const mockState = { const mockState = {
dashboardState: { dashboardState: {
@@ -86,9 +85,8 @@ describe('dashboardLayout actions', () => {
dashboardFilters.updateLayoutComponents.restore(); dashboardFilters.updateLayoutComponents.restore();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('updateComponents', () => { describe('updateComponents', () => {
test('should dispatch an updateLayout action', () => { it('should dispatch an updateLayout action', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const nextComponents = { 1: {} }; const nextComponents = { 1: {} };
const thunk = updateComponents(nextComponents); const thunk = updateComponents(nextComponents);
@@ -103,7 +101,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(0);
}); });
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -117,9 +115,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('deleteComponents', () => { describe('deleteComponents', () => {
test('should dispatch an deleteComponent action', () => { it('should dispatch an deleteComponent action', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const thunk = deleteComponent('id', 'parentId'); const thunk = deleteComponent('id', 'parentId');
thunk(dispatch, getState); thunk(dispatch, getState);
@@ -132,7 +129,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -145,9 +142,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('updateDashboardTitle', () => { describe('updateDashboardTitle', () => {
test('should dispatch an updateComponent action for the header component', () => { it('should dispatch an updateComponent action for the header component', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const thunk1 = updateDashboardTitle('new text'); const thunk1 = updateDashboardTitle('new text');
thunk1(dispatch, getState); thunk1(dispatch, getState);
@@ -171,9 +167,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('createTopLevelTabs', () => { describe('createTopLevelTabs', () => {
test('should dispatch a createTopLevelTabs action', () => { it('should dispatch a createTopLevelTabs action', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const dropResult = {}; const dropResult = {};
const thunk = createTopLevelTabs(dropResult); const thunk = createTopLevelTabs(dropResult);
@@ -187,7 +182,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -201,9 +196,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('deleteTopLevelTabs', () => { describe('deleteTopLevelTabs', () => {
test('should dispatch a deleteTopLevelTabs action', () => { it('should dispatch a deleteTopLevelTabs action', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const dropResult = {}; const dropResult = {};
const thunk = deleteTopLevelTabs(dropResult); const thunk = deleteTopLevelTabs(dropResult);
@@ -217,7 +211,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -231,7 +225,6 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('resizeComponent', () => { describe('resizeComponent', () => {
const dashboardLayout = { const dashboardLayout = {
...mockState.dashboardLayout, ...mockState.dashboardLayout,
@@ -247,7 +240,7 @@ describe('dashboardLayout actions', () => {
}, },
}; };
test('should update the size of the component', () => { it('should update the size of the component', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout, dashboardLayout,
}); });
@@ -278,7 +271,7 @@ describe('dashboardLayout actions', () => {
expect(dispatch.callCount).toBe(2); expect(dispatch.callCount).toBe(2);
}); });
test('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
dashboardLayout, dashboardLayout,
@@ -296,9 +289,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('handleComponentDrop', () => { describe('handleComponentDrop', () => {
test('should create a component if it is new', () => { it('should create a component if it is new', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const dropResult = { const dropResult = {
source: { id: NEW_COMPONENTS_SOURCE_ID }, source: { id: NEW_COMPONENTS_SOURCE_ID },
@@ -323,7 +315,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should move a component if the component is not new', () => { it('should move a component if the component is not new', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { dashboardLayout: {
// if 'dragging' is not only child will dispatch deleteComponent thunk // if 'dragging' is not only child will dispatch deleteComponent thunk
@@ -353,7 +345,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should dispatch a toast if the drop overflows the destination', () => { it('should dispatch a toast if the drop overflows the destination', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { dashboardLayout: {
present: { present: {
@@ -382,7 +374,7 @@ describe('dashboardLayout actions', () => {
expect(dispatch.callCount).toBe(1); expect(dispatch.callCount).toBe(1);
}); });
test('should delete a parent Row or Tabs if the moved child was the only child', () => { it('should delete a parent Row or Tabs if the moved child was the only child', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { dashboardLayout: {
present: { present: {
@@ -419,7 +411,7 @@ describe('dashboardLayout actions', () => {
}); });
}); });
test('should create top-level tabs if dropped on root', () => { it('should create top-level tabs if dropped on root', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const dropResult = { const dropResult = {
source: { id: NEW_COMPONENTS_SOURCE_ID }, source: { id: NEW_COMPONENTS_SOURCE_ID },
@@ -441,7 +433,7 @@ describe('dashboardLayout actions', () => {
}); });
}); });
test('should dispatch a toast if drop top-level tab into nested tab', () => { it('should dispatch a toast if drop top-level tab into nested tab', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { dashboardLayout: {
present: { present: {
@@ -496,9 +488,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('undoLayoutAction', () => { describe('undoLayoutAction', () => {
test('should dispatch a redux-undo .undo() action', () => { it('should dispatch a redux-undo .undo() action', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { past: ['non-empty'] }, dashboardLayout: { past: ['non-empty'] },
}); });
@@ -509,7 +500,7 @@ describe('dashboardLayout actions', () => {
expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.undo()); expect(dispatch.getCall(0).args[0]).toEqual(UndoActionCreators.undo());
}); });
test('should dispatch a setUnsavedChanges(false) action history length is zero', () => { it('should dispatch a setUnsavedChanges(false) action history length is zero', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardLayout: { past: [] }, dashboardLayout: { past: [] },
}); });
@@ -521,9 +512,8 @@ describe('dashboardLayout actions', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('redoLayoutAction', () => { describe('redoLayoutAction', () => {
test('should dispatch a redux-undo .redo() action', () => { it('should dispatch a redux-undo .redo() action', () => {
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const thunk = redoLayoutAction(); const thunk = redoLayoutAction();
thunk(dispatch, getState); thunk(dispatch, getState);
@@ -534,7 +524,7 @@ describe('dashboardLayout actions', () => {
expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1); expect(dashboardFilters.updateLayoutComponents.callCount).toEqual(1);
}); });
test('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => { it('should dispatch a setUnsavedChanges(true) action if hasUnsavedChanges=false', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });

View File

@@ -43,7 +43,6 @@ jest.mock('@superset-ui/core', () => ({
isFeatureEnabled: jest.fn(), isFeatureEnabled: jest.fn(),
})); }));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('dashboardState actions', () => { describe('dashboardState actions', () => {
const mockState = { const mockState = {
dashboardState: { dashboardState: {
@@ -102,9 +101,8 @@ describe('dashboardState actions', () => {
return { getState, dispatch, state }; return { getState, dispatch, state };
} }
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveDashboardRequest', () => { describe('saveDashboardRequest', () => {
test('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => { it('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -117,7 +115,7 @@ describe('dashboardState actions', () => {
expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED); expect(dispatch.getCall(1).args[0].type).toBe(SAVE_DASHBOARD_STARTED);
}); });
test('should post dashboard data with updated redux state', () => { it('should post dashboard data with updated redux state', () => {
const { getState, dispatch } = setup({ const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false }, dashboardState: { hasUnsavedChanges: false },
}); });
@@ -146,7 +144,6 @@ describe('dashboardState actions', () => {
).toStrictEqual(mockParentsList); ).toStrictEqual(mockParentsList);
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => { describe('FeatureFlag.CONFIRM_DASHBOARD_DIFF', () => {
beforeEach(() => { beforeEach(() => {
isFeatureEnabled.mockImplementation( isFeatureEnabled.mockImplementation(
@@ -158,7 +155,7 @@ describe('dashboardState actions', () => {
isFeatureEnabled.mockRestore(); isFeatureEnabled.mockRestore();
}); });
test('dispatches SET_OVERRIDE_CONFIRM when an inspect value has diff', async () => { it('dispatches SET_OVERRIDE_CONFIRM when an inspect value has diff', async () => {
const id = 192; const id = 192;
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const thunk = saveDashboardRequest( const thunk = saveDashboardRequest(
@@ -177,7 +174,7 @@ describe('dashboardState actions', () => {
).toBe(id); ).toBe(id);
}); });
test('should post dashboard data with after confirm the overwrite values', async () => { it('should post dashboard data with after confirm the overwrite values', async () => {
const id = 192; const id = 192;
const { getState, dispatch } = setup(); const { getState, dispatch } = setup();
const confirmedDashboardData = { const confirmedDashboardData = {

View File

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

View File

@@ -19,7 +19,6 @@
import { render, act } from 'spec/helpers/testing-library'; import { render, act } from 'spec/helpers/testing-library';
import AnchorLink from 'src/dashboard/components/AnchorLink'; import AnchorLink from 'src/dashboard/components/AnchorLink';
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('AnchorLink', () => { describe('AnchorLink', () => {
const props = { const props = {
id: 'CHART-123', id: 'CHART-123',
@@ -31,7 +30,7 @@ describe('AnchorLink', () => {
window.location = globalLocation; window.location = globalLocation;
}); });
test('should scroll the AnchorLink into view upon mount if id matches hash', async () => { it('should scroll the AnchorLink into view upon mount if id matches hash', async () => {
const callback = jest.fn(); const callback = jest.fn();
jest.spyOn(document, 'getElementById').mockReturnValue({ jest.spyOn(document, 'getElementById').mockReturnValue({
scrollIntoView: callback, scrollIntoView: callback,
@@ -50,7 +49,7 @@ describe('AnchorLink', () => {
expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1);
}); });
test('should render anchor link without short link button', () => { it('should render anchor link without short link button', () => {
const { container, queryByRole } = render( const { container, queryByRole } = render(
<AnchorLink showShortLinkButton={false} {...props} />, <AnchorLink showShortLinkButton={false} {...props} />,
{ useRedux: true }, { useRedux: true },
@@ -59,7 +58,7 @@ describe('AnchorLink', () => {
expect(queryByRole('button')).not.toBeInTheDocument(); expect(queryByRole('button')).not.toBeInTheDocument();
}); });
test('should render short link button', () => { it('should render short link button', () => {
const { getByRole } = render( const { getByRole } = render(
<AnchorLink {...props} showShortLinkButton />, <AnchorLink {...props} showShortLinkButton />,
{ useRedux: true }, { useRedux: true },

View File

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

View File

@@ -120,12 +120,15 @@ class Dashboard extends PureComponent {
this.applyCharts(); this.applyCharts();
} }
componentDidUpdate(prevProps) { componentDidUpdate() {
this.applyCharts(); this.applyCharts();
const currentChartIds = getChartIdsFromLayout(prevProps.layout); }
const nextChartIds = getChartIdsFromLayout(this.props.layout);
if (prevProps.dashboardId !== this.props.dashboardId) { UNSAFE_componentWillReceiveProps(nextProps) {
const currentChartIds = getChartIdsFromLayout(this.props.layout);
const nextChartIds = getChartIdsFromLayout(nextProps.layout);
if (this.props.dashboardId !== nextProps.dashboardId) {
// single-page-app navigation check // single-page-app navigation check
return; return;
} }
@@ -137,7 +140,7 @@ class Dashboard extends PureComponent {
newChartIds.forEach(newChartId => newChartIds.forEach(newChartId =>
this.props.actions.addSliceToDashboard( this.props.actions.addSliceToDashboard(
newChartId, newChartId,
getLayoutComponentFromChartId(this.props.layout, newChartId), getLayoutComponentFromChartId(nextProps.layout, newChartId),
), ),
); );
} else if (currentChartIds.length > nextChartIds.length) { } else if (currentChartIds.length > nextChartIds.length) {

View File

@@ -39,7 +39,6 @@ import { getRelatedCharts } from 'src/dashboard/util/getRelatedCharts';
jest.mock('src/dashboard/util/getRelatedCharts'); jest.mock('src/dashboard/util/getRelatedCharts');
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('Dashboard', () => { describe('Dashboard', () => {
const mockAddSlice = jest.fn(); const mockAddSlice = jest.fn();
const mockRemoveSlice = jest.fn(); const mockRemoveSlice = jest.fn();
@@ -92,19 +91,18 @@ describe('Dashboard', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
test('should render the children component', () => { it('should render the children component', () => {
renderDashboard(); renderDashboard();
expect(screen.getByText('Test')).toBeInTheDocument(); expect(screen.getByText('Test')).toBeInTheDocument();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('layout changes', () => { describe('layout changes', () => {
const layoutWithExtraChart = { const layoutWithExtraChart = {
...props.layout, ...props.layout,
1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }), 1001: newComponentFactory(CHART_TYPE, { chartId: 1001 }),
}; };
test('should call addSliceToDashboard if a new slice is added to the layout', () => { it('should call addSliceToDashboard if a new slice is added to the layout', () => {
const { rerender } = renderDashboard(); const { rerender } = renderDashboard();
rerender( rerender(
@@ -118,7 +116,7 @@ describe('Dashboard', () => {
expect(mockAddSlice).toHaveBeenCalled(); expect(mockAddSlice).toHaveBeenCalled();
}); });
test('should call removeSliceFromDashboard if a slice is removed from the layout', () => { it('should call removeSliceFromDashboard if a slice is removed from the layout', () => {
const { rerender } = renderDashboard({ layout: layoutWithExtraChart }); const { rerender } = renderDashboard({ layout: layoutWithExtraChart });
const nextLayout = { ...layoutWithExtraChart }; const nextLayout = { ...layoutWithExtraChart };
@@ -136,9 +134,8 @@ describe('Dashboard', () => {
}); });
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('filter updates', () => { describe('filter updates', () => {
test('should not call refresh when in editMode', () => { it('should not call refresh when in editMode', () => {
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
rerender( rerender(
@@ -159,7 +156,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).not.toHaveBeenCalled(); expect(mockTriggerQuery).not.toHaveBeenCalled();
}); });
test('should not call refresh when there is no change', () => { it('should not call refresh when there is no change', () => {
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
rerender( rerender(
@@ -173,7 +170,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).not.toHaveBeenCalled(); expect(mockTriggerQuery).not.toHaveBeenCalled();
}); });
test('should call refresh when native filters changed', () => { it('should call refresh when native filters changed', () => {
getRelatedCharts.mockReturnValue([230]); getRelatedCharts.mockReturnValue([230]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -198,7 +195,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled(); expect(mockTriggerQuery).toHaveBeenCalled();
}); });
test('should call refresh if a filter is added', () => { it('should call refresh if a filter is added', () => {
getRelatedCharts.mockReturnValue([1]); getRelatedCharts.mockReturnValue([1]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -217,7 +214,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled(); expect(mockTriggerQuery).toHaveBeenCalled();
}); });
test('should call refresh if a filter is removed', () => { it('should call refresh if a filter is removed', () => {
getRelatedCharts.mockReturnValue([1]); // Ensure we return some charts to refresh getRelatedCharts.mockReturnValue([1]); // Ensure we return some charts to refresh
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -236,7 +233,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalledWith(true, 1); expect(mockTriggerQuery).toHaveBeenCalledWith(true, 1);
}); });
test('should call refresh if a filter is changed', () => { it('should call refresh if a filter is changed', () => {
getRelatedCharts.mockReturnValue([1]); getRelatedCharts.mockReturnValue([1]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -256,7 +253,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled(); expect(mockTriggerQuery).toHaveBeenCalled();
}); });
test('should call refresh with multiple chart ids', () => { it('should call refresh with multiple chart ids', () => {
getRelatedCharts.mockReturnValue([1, 2]); getRelatedCharts.mockReturnValue([1, 2]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -276,7 +273,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled(); expect(mockTriggerQuery).toHaveBeenCalled();
}); });
test('should call refresh if a filter scope is changed', () => { it('should call refresh if a filter scope is changed', () => {
getRelatedCharts.mockReturnValue([2]); getRelatedCharts.mockReturnValue([2]);
const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS }); const { rerender } = renderDashboard({ activeFilters: OVERRIDE_FILTERS });
@@ -296,7 +293,7 @@ describe('Dashboard', () => {
expect(mockTriggerQuery).toHaveBeenCalled(); expect(mockTriggerQuery).toHaveBeenCalled();
}); });
test('should call refresh with empty [] if a filter is changed but scope is not applicable', () => { it('should call refresh with empty [] if a filter is changed but scope is not applicable', () => {
getRelatedCharts.mockReturnValue([]); getRelatedCharts.mockReturnValue([]);
const { rerender } = renderDashboard({ const { rerender } = renderDashboard({
activeFilters: OVERRIDE_FILTERS, activeFilters: OVERRIDE_FILTERS,

View File

@@ -83,7 +83,6 @@ jest.mock('src/dashboard/containers/DashboardGrid', () => () => (
<div data-test="mock-dashboard-grid" /> <div data-test="mock-dashboard-grid" />
)); ));
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('DashboardBuilder', () => { describe('DashboardBuilder', () => {
let favStarStub: jest.Mock; let favStarStub: jest.Mock;
let activeTabsStub: jest.Mock; let activeTabsStub: jest.Mock;
@@ -121,13 +120,13 @@ describe('DashboardBuilder', () => {
}); });
} }
test('should render a StickyContainer with class "dashboard"', () => { it('should render a StickyContainer with class "dashboard"', () => {
const { getByTestId } = setup(); const { getByTestId } = setup();
const stickyContainer = getByTestId('dashboard-content-wrapper'); const stickyContainer = getByTestId('dashboard-content-wrapper');
expect(stickyContainer).toHaveClass('dashboard'); expect(stickyContainer).toHaveClass('dashboard');
}); });
test('should add the "dashboard--editing" class if editMode=true', () => { it('should add the "dashboard--editing" class if editMode=true', () => {
const { getByTestId } = setup({ const { getByTestId } = setup({
dashboardState: { ...mockState.dashboardState, editMode: true }, dashboardState: { ...mockState.dashboardState, editMode: true },
}); });
@@ -135,13 +134,13 @@ describe('DashboardBuilder', () => {
expect(stickyContainer).toHaveClass('dashboard dashboard--editing'); expect(stickyContainer).toHaveClass('dashboard dashboard--editing');
}); });
test('should render a DragDroppable DashboardHeader', () => { it('should render a DragDroppable DashboardHeader', () => {
const { queryByTestId } = setup(); const { queryByTestId } = setup();
const header = queryByTestId('dashboard-header-container'); const header = queryByTestId('dashboard-header-container');
expect(header).toBeInTheDocument(); expect(header).toBeInTheDocument();
}); });
test('should render a Sticky top-level Tabs if the dashboard has tabs', async () => { it('should render a Sticky top-level Tabs if the dashboard has tabs', async () => {
const { findAllByTestId } = setup({ const { findAllByTestId } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs, dashboardLayout: undoableDashboardLayoutWithTabs,
}); });
@@ -163,7 +162,7 @@ describe('DashboardBuilder', () => {
}); });
}); });
test('should render one Tabs and two TabPane', async () => { it('should render one Tabs and two TabPane', async () => {
const { findAllByRole } = setup({ const { findAllByRole } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs, dashboardLayout: undoableDashboardLayoutWithTabs,
}); });
@@ -173,7 +172,7 @@ describe('DashboardBuilder', () => {
expect(tabPanels.length).toBe(2); expect(tabPanels.length).toBe(2);
}); });
test('should render a TabPane and DashboardGrid for first Tab', async () => { it('should render a TabPane and DashboardGrid for first Tab', async () => {
const { findByTestId } = setup({ const { findByTestId } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs, dashboardLayout: undoableDashboardLayoutWithTabs,
}); });
@@ -189,7 +188,7 @@ describe('DashboardBuilder', () => {
).toBe(1); ).toBe(1);
}); });
test('should render a TabPane and DashboardGrid for second Tab', async () => { it('should render a TabPane and DashboardGrid for second Tab', async () => {
const { findByTestId } = setup({ const { findByTestId } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs, dashboardLayout: undoableDashboardLayoutWithTabs,
dashboardState: { dashboardState: {
@@ -210,13 +209,13 @@ describe('DashboardBuilder', () => {
).toBe(1); ).toBe(1);
}); });
test('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => { it('should render a BuilderComponentPane if editMode=false and user selects "Insert Components" pane', () => {
const { queryAllByTestId } = setup(); const { queryAllByTestId } = setup();
const builderComponents = queryAllByTestId('mock-builder-component-pane'); const builderComponents = queryAllByTestId('mock-builder-component-pane');
expect(builderComponents.length).toBe(0); expect(builderComponents.length).toBe(0);
}); });
test('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => { it('should render a BuilderComponentPane if editMode=true and user selects "Insert Components" pane', () => {
const { queryAllByTestId } = setup({ const { queryAllByTestId } = setup({
dashboardState: { ...mockState.dashboardState, editMode: true }, dashboardState: { ...mockState.dashboardState, editMode: true },
}); });
@@ -224,7 +223,7 @@ describe('DashboardBuilder', () => {
expect(builderComponents.length).toBeGreaterThanOrEqual(1); expect(builderComponents.length).toBeGreaterThanOrEqual(1);
}); });
test('should change redux state if a top-level Tab is clicked', async () => { it('should change redux state if a top-level Tab is clicked', async () => {
(setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({ (setDirectPathToChild as jest.Mock).mockImplementation(arg0 => ({
type: 'type', type: 'type',
arg0, arg0,
@@ -244,13 +243,13 @@ describe('DashboardBuilder', () => {
(setDirectPathToChild as jest.Mock).mockReset(); (setDirectPathToChild as jest.Mock).mockReset();
}); });
test('should not display a loading spinner when saving is not in progress', () => { it('should not display a loading spinner when saving is not in progress', () => {
const { queryByTestId } = setup(); const { queryByTestId } = setup();
expect(queryByTestId('loading-indicator')).not.toBeInTheDocument(); expect(queryByTestId('loading-indicator')).not.toBeInTheDocument();
}); });
test('should display a loading spinner when saving is in progress', async () => { it('should display a loading spinner when saving is in progress', async () => {
const { findByTestId } = setup({ const { findByTestId } = setup({
dashboardState: { ...mockState.dashboardState, dashboardIsSaving: true }, dashboardState: { ...mockState.dashboardState, dashboardIsSaving: true },
}); });
@@ -258,7 +257,7 @@ describe('DashboardBuilder', () => {
expect(await findByTestId('loading-indicator')).toBeVisible(); expect(await findByTestId('loading-indicator')).toBeVisible();
}); });
test('should set FilterBar width by useStoredSidebarWidth', () => { it('should set FilterBar width by useStoredSidebarWidth', () => {
const expectedValue = 200; const expectedValue = 200;
const setter = jest.fn(); const setter = jest.fn();
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [ (useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
@@ -275,7 +274,7 @@ describe('DashboardBuilder', () => {
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`); expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
}); });
test('filter panel state when featureflag is true', () => { it('filter panel state when featureflag is true', () => {
window.featureFlags = { window.featureFlags = {
[FeatureFlag.FilterBarClosedByDefault]: true, [FeatureFlag.FilterBarClosedByDefault]: true,
}; };
@@ -295,7 +294,7 @@ describe('DashboardBuilder', () => {
expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`); expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`);
}); });
test('filter panel state when featureflag is false', () => { it('filter panel state when featureflag is false', () => {
window.featureFlags = { window.featureFlags = {
[FeatureFlag.FilterBarClosedByDefault]: false, [FeatureFlag.FilterBarClosedByDefault]: false,
}; };
@@ -315,7 +314,7 @@ describe('DashboardBuilder', () => {
expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`); expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`);
}); });
test('should not render the filter bar when nativeFiltersEnabled is false', () => { it('should not render the filter bar when nativeFiltersEnabled is false', () => {
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({ jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
showDashboard: true, showDashboard: true,
missingInitialFilters: [], missingInitialFilters: [],
@@ -328,7 +327,7 @@ describe('DashboardBuilder', () => {
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument(); expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
}); });
test('should render the filter bar when nativeFiltersEnabled is true and not in edit mode', () => { it('should render the filter bar when nativeFiltersEnabled is true and not in edit mode', () => {
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({ jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
showDashboard: true, showDashboard: true,
missingInitialFilters: [], missingInitialFilters: [],
@@ -341,7 +340,7 @@ describe('DashboardBuilder', () => {
expect(queryByTestId('dashboard-filters-panel')).toBeInTheDocument(); expect(queryByTestId('dashboard-filters-panel')).toBeInTheDocument();
}); });
test('should not render the filter bar when in edit mode even if nativeFiltersEnabled is true', () => { it('should not render the filter bar when in edit mode even if nativeFiltersEnabled is true', () => {
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({ jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
showDashboard: true, showDashboard: true,
missingInitialFilters: [], missingInitialFilters: [],

View File

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

View File

@@ -86,6 +86,16 @@ const StyledFilterCount = styled.div`
const StyledBadge = styled(Badge)` const StyledBadge = styled(Badge)`
${({ theme }) => ` ${({ theme }) => `
margin-left: ${theme.sizeUnit * 2}px; margin-left: ${theme.sizeUnit * 2}px;
&>sup.ant-badge-count {
padding: 0 ${theme.sizeUnit}px;
min-width: ${theme.sizeUnit * 4}px;
height: ${theme.sizeUnit * 4}px;
line-height: 1.5;
font-weight: ${theme.fontWeightStrong};
font-size: ${theme.fontSizeSM - 1}px;
box-shadow: none;
padding: 0 ${theme.sizeUnit}px;
}
`} `}
`; `;
@@ -304,7 +314,6 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
data-test="applied-filter-count" data-test="applied-filter-count"
className="applied-count" className="applied-count"
count={filterCount} count={filterCount}
size="small"
showZero showZero
/> />
</StyledFilterCount> </StyledFilterCount>

View File

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

View File

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

View File

@@ -180,7 +180,6 @@ afterAll(() => {
fetchMock.restore(); fetchMock.restore();
}); });
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('PropertiesModal', () => { describe('PropertiesModal', () => {
jest.setTimeout(60000); // Increased timeout for complex modal rendering jest.setTimeout(60000); // Increased timeout for complex modal rendering
@@ -355,19 +354,9 @@ describe('PropertiesModal', () => {
mockedIsFeatureEnabled.mockReturnValue(false); mockedIsFeatureEnabled.mockReturnValue(false);
const props = createProps(); const props = createProps();
props.onlyApply = false; props.onlyApply = false;
// Pass dashboardInfo to avoid loading state render(<PropertiesModal {...props} />, {
const propsWithDashboardInfo = {
...props,
dashboardInfo: {
...dashboardInfo,
json_metadata: mockedJsonMetadata,
},
};
render(<PropertiesModal {...propsWithDashboardInfo} />, {
useRedux: true, useRedux: true,
}); });
// Wait for the form to be visible
expect( expect(
await screen.findByTestId('dashboard-edit-properties-form'), await screen.findByTestId('dashboard-edit-properties-form'),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -390,19 +379,9 @@ describe('PropertiesModal', () => {
mockedIsFeatureEnabled.mockReturnValue(false); mockedIsFeatureEnabled.mockReturnValue(false);
const props = createProps(); const props = createProps();
props.onlyApply = true; props.onlyApply = true;
// Pass dashboardInfo to avoid loading state render(<PropertiesModal {...props} />, {
const propsWithDashboardInfo = {
...props,
dashboardInfo: {
...dashboardInfo,
json_metadata: mockedJsonMetadata,
},
};
render(<PropertiesModal {...propsWithDashboardInfo} />, {
useRedux: true, useRedux: true,
}); });
// Wait for the form to be visible
expect( expect(
await screen.findByTestId('dashboard-edit-properties-form'), await screen.findByTestId('dashboard-edit-properties-form'),
).toBeInTheDocument(); ).toBeInTheDocument();

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