mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
feat(frontend): add dataset cache clearing utilities and integration (#35264)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Geidō <60598000+geido@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9fbfcf0ccd
commit
0b535b792e
@@ -61,6 +61,10 @@ jest.mock('src/SqlLab/actions/sqlLab', () => ({
|
||||
jest.mock('src/explore/exploreUtils/formData', () => ({
|
||||
postFormData: jest.fn(),
|
||||
}));
|
||||
jest.mock('src/utils/cachedSupersetGet', () => ({
|
||||
...jest.requireActual('src/utils/cachedSupersetGet'),
|
||||
clearDatasetCache: jest.fn(),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('SaveDatasetModal', () => {
|
||||
@@ -336,4 +340,42 @@ describe('SaveDatasetModal', () => {
|
||||
templateParams: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('clears dataset cache when creating new dataset', async () => {
|
||||
const clearDatasetCache = jest.spyOn(
|
||||
require('src/utils/cachedSupersetGet'),
|
||||
'clearDatasetCache',
|
||||
);
|
||||
const postFormData = jest.spyOn(
|
||||
require('src/explore/exploreUtils/formData'),
|
||||
'postFormData',
|
||||
);
|
||||
|
||||
const dummyDispatch = jest.fn().mockResolvedValue({ id: 123 });
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue({ ...user });
|
||||
postFormData.mockResolvedValue('chart_key_123');
|
||||
|
||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||
|
||||
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
|
||||
fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
|
||||
|
||||
const saveConfirmationBtn = screen.getByRole('button', {
|
||||
name: /save/i,
|
||||
});
|
||||
userEvent.click(saveConfirmationBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(clearDatasetCache).toHaveBeenCalledWith(123);
|
||||
});
|
||||
});
|
||||
|
||||
test('clearDatasetCache is imported and available', () => {
|
||||
const clearDatasetCache =
|
||||
require('src/utils/cachedSupersetGet').clearDatasetCache;
|
||||
|
||||
expect(clearDatasetCache).toBeDefined();
|
||||
expect(typeof clearDatasetCache).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ import { mountExploreUrl } from 'src/explore/exploreUtils';
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { clearDatasetCache } from 'src/utils/cachedSupersetGet';
|
||||
|
||||
interface QueryDatabase {
|
||||
id?: number;
|
||||
@@ -170,6 +171,9 @@ const updateDataset = async (
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
clearDatasetCache(datasetId);
|
||||
|
||||
return data.json.result;
|
||||
};
|
||||
|
||||
@@ -347,15 +351,17 @@ export const SaveDatasetModal = ({
|
||||
datasourceName: datasetName,
|
||||
}),
|
||||
)
|
||||
.then((data: { id: number }) =>
|
||||
postFormData(data.id, 'table', {
|
||||
.then((data: { id: number }) => {
|
||||
clearDatasetCache(data.id);
|
||||
|
||||
return postFormData(data.id, 'table', {
|
||||
...formDataWithDefaults,
|
||||
datasource: `${data.id}__table`,
|
||||
...(defaultVizType === VizType.Table && {
|
||||
all_columns: selectedColumns.map(column => column.column_name),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
});
|
||||
})
|
||||
.then((key: string) => {
|
||||
setLoading(false);
|
||||
const url = mountExploreUrl(null, {
|
||||
|
||||
@@ -193,6 +193,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
|
||||
const { json } = await SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/${currentDatasource?.id}`,
|
||||
});
|
||||
|
||||
addSuccessToast(t('The dataset has been saved'));
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
json.result.type = 'table';
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
resetDatabaseState,
|
||||
} from 'src/database/actions';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { clearDatasetCache } from 'src/utils/cachedSupersetGet';
|
||||
import { DatabaseSelector } from '../../../DatabaseSelector';
|
||||
import CollectionTable from '../CollectionTable';
|
||||
import Fieldset from '../Fieldset';
|
||||
@@ -840,6 +841,9 @@ class DatasourceEditor extends PureComponent {
|
||||
col => !col.expression, // remove calculated columns
|
||||
),
|
||||
});
|
||||
|
||||
clearDatasetCache(datasource.id);
|
||||
|
||||
this.props.addSuccessToast(t('Metadata has been synced'));
|
||||
this.setState({ metadataLoading: false });
|
||||
} catch (error) {
|
||||
|
||||
190
superset-frontend/src/utils/cachedSupersetGet.test.ts
Normal file
190
superset-frontend/src/utils/cachedSupersetGet.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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 {
|
||||
supersetGetCache,
|
||||
clearDatasetCache,
|
||||
clearAllDatasetCache,
|
||||
} from './cachedSupersetGet';
|
||||
|
||||
describe('cachedSupersetGet', () => {
|
||||
beforeEach(() => {
|
||||
supersetGetCache.clear();
|
||||
});
|
||||
|
||||
describe('clearDatasetCache', () => {
|
||||
test('clears cache entries for specific dataset ID', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
supersetGetCache.set('/api/v1/dataset/123/', { data: 'dataset123slash' });
|
||||
supersetGetCache.set('/api/v1/dataset/123?query=1', {
|
||||
data: 'dataset123query',
|
||||
});
|
||||
supersetGetCache.set('/api/v1/dataset/456', { data: 'dataset456' });
|
||||
supersetGetCache.set('/api/v1/other/123', { data: 'other' });
|
||||
|
||||
clearDatasetCache(123);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123/')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123?query=1')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/456')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/other/123')).toBe(true);
|
||||
});
|
||||
|
||||
test('clears cache entries for string dataset ID', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/abc-123', { data: 'datasetAbc' });
|
||||
supersetGetCache.set('/api/v1/dataset/abc-123/', {
|
||||
data: 'datasetAbcSlash',
|
||||
});
|
||||
supersetGetCache.set('/api/v1/dataset/def-456', { data: 'datasetDef' });
|
||||
|
||||
clearDatasetCache('abc-123');
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/abc-123')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/abc-123/')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/def-456')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles null dataset ID gracefully', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
|
||||
clearDatasetCache(null as any);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles undefined dataset ID gracefully', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
|
||||
clearDatasetCache(undefined as any);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles empty string dataset ID gracefully', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
|
||||
clearDatasetCache('');
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(true);
|
||||
});
|
||||
|
||||
test('does not clear unrelated cache entries', () => {
|
||||
supersetGetCache.set('/api/v1/chart/123', { data: 'chart123' });
|
||||
supersetGetCache.set('/api/v1/dashboard/123', { data: 'dashboard123' });
|
||||
supersetGetCache.set('/api/v1/database/123', { data: 'database123' });
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
|
||||
clearDatasetCache(123);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/chart/123')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dashboard/123')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/database/123')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(false);
|
||||
});
|
||||
|
||||
test('only clears exact dataset ID matches', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/1', { data: 'dataset1' });
|
||||
supersetGetCache.set('/api/v1/dataset/12', { data: 'dataset12' });
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
supersetGetCache.set('/api/v1/dataset/1234', { data: 'dataset1234' });
|
||||
supersetGetCache.set('/api/v1/dataset/456', { data: 'dataset456' });
|
||||
|
||||
clearDatasetCache(123);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/1')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/12')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/1234')).toBe(true); // Should not be cleared - different ID
|
||||
expect(supersetGetCache.has('/api/v1/dataset/456')).toBe(true);
|
||||
});
|
||||
|
||||
test('clears cache entries with various URL patterns', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/789', { data: 'base' });
|
||||
supersetGetCache.set('/api/v1/dataset/789/columns', { data: 'columns' });
|
||||
supersetGetCache.set('/api/v1/dataset/789/related', { data: 'related' });
|
||||
supersetGetCache.set('/api/v1/dataset/789?full=true', {
|
||||
data: 'withQuery',
|
||||
});
|
||||
|
||||
clearDatasetCache(789);
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/789')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/789/columns')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/789/related')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/789?full=true')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAllDatasetCache', () => {
|
||||
test('clears all dataset cache entries', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/123', { data: 'dataset123' });
|
||||
supersetGetCache.set('/api/v1/dataset/456', { data: 'dataset456' });
|
||||
supersetGetCache.set('/api/v1/dataset/789/columns', { data: 'columns' });
|
||||
supersetGetCache.set('/api/v1/chart/123', { data: 'chart123' });
|
||||
supersetGetCache.set('/api/v1/dashboard/456', { data: 'dashboard456' });
|
||||
|
||||
clearAllDatasetCache();
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/123')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/456')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/789/columns')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/chart/123')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dashboard/456')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles empty cache gracefully', () => {
|
||||
expect(supersetGetCache.size).toBe(0);
|
||||
|
||||
clearAllDatasetCache();
|
||||
|
||||
expect(supersetGetCache.size).toBe(0);
|
||||
});
|
||||
|
||||
test('preserves non-dataset cache entries', () => {
|
||||
supersetGetCache.set('/api/v1/chart/list', { data: 'chartList' });
|
||||
supersetGetCache.set('/api/v1/dashboard/list', { data: 'dashboardList' });
|
||||
supersetGetCache.set('/api/v1/database/list', { data: 'databaseList' });
|
||||
supersetGetCache.set('/api/v1/query/list', { data: 'queryList' });
|
||||
|
||||
clearAllDatasetCache();
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/chart/list')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/dashboard/list')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/database/list')).toBe(true);
|
||||
expect(supersetGetCache.has('/api/v1/query/list')).toBe(true);
|
||||
});
|
||||
|
||||
test('clears all variations of dataset endpoints', () => {
|
||||
supersetGetCache.set('/api/v1/dataset/', { data: 'list' });
|
||||
supersetGetCache.set('/api/v1/dataset/export', { data: 'export' });
|
||||
supersetGetCache.set('/api/v1/dataset/import', { data: 'import' });
|
||||
supersetGetCache.set('/api/v1/dataset/duplicate', { data: 'duplicate' });
|
||||
supersetGetCache.set('/api/v1/dataset/1/refresh', { data: 'refresh' });
|
||||
|
||||
clearAllDatasetCache();
|
||||
|
||||
expect(supersetGetCache.has('/api/v1/dataset/')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/export')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/import')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/duplicate')).toBe(false);
|
||||
expect(supersetGetCache.has('/api/v1/dataset/1/refresh')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -27,3 +27,59 @@ export const cachedSupersetGet = cacheWrapper(
|
||||
supersetGetCache,
|
||||
({ endpoint }) => endpoint || '',
|
||||
);
|
||||
|
||||
/**
|
||||
* Clear cached responses for dataset-related endpoints
|
||||
* @param datasetId - The ID of the dataset to clear from cache
|
||||
*/
|
||||
export function clearDatasetCache(datasetId: number | string): void {
|
||||
if (datasetId === null || datasetId === undefined || datasetId === '') return;
|
||||
|
||||
const datasetIdStr = String(datasetId);
|
||||
|
||||
supersetGetCache.forEach((_value, key) => {
|
||||
// Match exact dataset ID patterns:
|
||||
// - /api/v1/dataset/123 (exact match or end of URL)
|
||||
// - /api/v1/dataset/123/ (with trailing slash)
|
||||
// - /api/v1/dataset/123? (with query params)
|
||||
const patterns = [
|
||||
`/api/v1/dataset/${datasetIdStr}`,
|
||||
`/api/v1/dataset/${datasetIdStr}/`,
|
||||
`/api/v1/dataset/${datasetIdStr}?`,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
if (key.includes(pattern)) {
|
||||
// Additional check to ensure we don't match longer IDs
|
||||
const afterPattern = key.substring(
|
||||
key.indexOf(pattern) + pattern.length,
|
||||
);
|
||||
// If pattern ends with slash or query, it's already precise
|
||||
if (pattern.endsWith('/') || pattern.endsWith('?')) {
|
||||
supersetGetCache.delete(key);
|
||||
break;
|
||||
}
|
||||
// For the base pattern, ensure nothing follows or only valid separators
|
||||
if (
|
||||
afterPattern === '' ||
|
||||
afterPattern.startsWith('/') ||
|
||||
afterPattern.startsWith('?')
|
||||
) {
|
||||
supersetGetCache.delete(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached dataset responses
|
||||
*/
|
||||
export function clearAllDatasetCache(): void {
|
||||
supersetGetCache.forEach((_value, key) => {
|
||||
if (key.includes('/api/v1/dataset/')) {
|
||||
supersetGetCache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user