diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index e3dc951948a..29531a652e4 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -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(, { 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');
+ });
});
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index 721b9081873..17c5de8085e 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -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, {
diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
index 32fcf6f977d..96e271f9e59 100644
--- a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
+++ b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx
@@ -193,6 +193,7 @@ const DatasourceModal: FunctionComponent = ({
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';
diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
index d5a9b26dcce..30d5a824281 100644
--- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.jsx
@@ -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) {
diff --git a/superset-frontend/src/utils/cachedSupersetGet.test.ts b/superset-frontend/src/utils/cachedSupersetGet.test.ts
new file mode 100644
index 00000000000..1ec63437c8b
--- /dev/null
+++ b/superset-frontend/src/utils/cachedSupersetGet.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/superset-frontend/src/utils/cachedSupersetGet.ts b/superset-frontend/src/utils/cachedSupersetGet.ts
index 2f319cac350..c4ab529c0f4 100644
--- a/superset-frontend/src/utils/cachedSupersetGet.ts
+++ b/superset-frontend/src/utils/cachedSupersetGet.ts
@@ -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);
+ }
+ });
+}