mirror of
https://github.com/apache/superset.git
synced 2026-07-07 15:25:36 +00:00
Compare commits
3 Commits
chart-samp
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebdd3f29f7 | ||
|
|
ad8e6d4140 | ||
|
|
83d93b8b42 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -10,11 +10,7 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
- dependency-name: "react-error-boundary"
|
||||
- dependency-name: "@rjsf/*"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-markdown"
|
||||
# TODO: remove below entries until React >= 19.0.0
|
||||
- dependency-name: "react-icons"
|
||||
# JSDOM v30 doesn't play well with Jest v30
|
||||
@@ -78,8 +74,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
ignore:
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -376,11 +370,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-core/"
|
||||
ignore:
|
||||
# not until React >= 18.0.0
|
||||
- dependency-name: "react-markdown"
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
|
||||
@@ -140,7 +140,7 @@ d1 = [
|
||||
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector==4.2.6",
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
"databricks-sqlalchemy==2.0.9",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
|
||||
|
||||
2627
superset-frontend/package-lock.json
generated
2627
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -393,7 +393,6 @@
|
||||
"http-proxy-middleware": "^2.0.10",
|
||||
"tar": "^7.5.16",
|
||||
"puppeteer": "^22.4.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"underscore": "^1.13.7",
|
||||
"jspdf": "^4.2.0",
|
||||
"nwsapi": "^2.2.13",
|
||||
|
||||
@@ -53,16 +53,16 @@
|
||||
"re-resizable": "^6.11.2",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-draggable": "^4.7.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-markdown": "^9.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^5.2.0",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
// TODO: Upgrade to remark-gfm v4+ after migrating to React 18.
|
||||
// remark-gfm v4+ requires react-markdown v9+, which requires React 18.
|
||||
// Currently pinned to v3.0.1 for compatibility with react-markdown v8 and React 17.
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { cloneDeep, mergeWith } from 'lodash-es';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../../utils';
|
||||
@@ -139,7 +136,7 @@ export function SafeMarkdown({
|
||||
rehypePlugins={rehypePlugins}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={false}
|
||||
transformLinkUri={transformLinkUri}
|
||||
urlTransform={transformLinkUri}
|
||||
>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -26,7 +26,12 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { getExtensionsRegistry, QueryData, VizType } from '@superset-ui/core';
|
||||
import {
|
||||
getExtensionsRegistry,
|
||||
JsonObject,
|
||||
QueryData,
|
||||
VizType,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
css,
|
||||
styled,
|
||||
@@ -58,6 +63,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
|
||||
filters: object;
|
||||
handleToggleFullSize: () => void;
|
||||
formData: object;
|
||||
ownState?: JsonObject;
|
||||
width: number;
|
||||
height: number;
|
||||
queriedDttm?: string | null;
|
||||
@@ -174,6 +180,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
height,
|
||||
exportPivotExcel = () => ({}),
|
||||
chartHolderRef,
|
||||
ownState,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -381,6 +388,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
crossFiltersEnabled={isCrossFiltersEnabled}
|
||||
exportPivotExcel={exportPivotExcel}
|
||||
chartHolderRef={chartHolderRef}
|
||||
ownState={ownState}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
getChartMetadataRegistry,
|
||||
VizType,
|
||||
BinaryQueryObjectFilterClause,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { css, useTheme, styled } from '@apache-superset/core/theme';
|
||||
@@ -140,6 +141,8 @@ export interface SliceHeaderControlsProps {
|
||||
supersetCanDownload?: boolean;
|
||||
|
||||
crossFiltersEnabled?: boolean;
|
||||
|
||||
ownState?: JsonObject;
|
||||
}
|
||||
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
|
||||
RouteComponentProps;
|
||||
@@ -486,7 +489,12 @@ const SliceHeaderControls = (
|
||||
<div data-test="view-query-menu-item">{t('View query')}</div>
|
||||
}
|
||||
modalTitle={t('View query')}
|
||||
modalBody={<ViewQueryModal latestQueryFormData={props.formData} />}
|
||||
modalBody={
|
||||
<ViewQueryModal
|
||||
latestQueryFormData={props.formData}
|
||||
ownState={props.ownState}
|
||||
/>
|
||||
}
|
||||
draggable
|
||||
resizable
|
||||
responsive
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
||||
|
||||
export interface SliceHeaderControlsProps {
|
||||
slice: {
|
||||
@@ -60,4 +60,5 @@ export interface SliceHeaderControlsProps {
|
||||
supersetCanDownload?: boolean;
|
||||
|
||||
crossFiltersEnabled?: boolean;
|
||||
ownState?: JsonObject;
|
||||
}
|
||||
|
||||
@@ -480,6 +480,26 @@ const Chart = (props: ChartProps) => {
|
||||
|
||||
(formData as JsonObject).dashboardId = dashboardInfo.id;
|
||||
|
||||
// Memoize ownState so it keeps a stable reference across re-renders that
|
||||
// don't change its logical value. ViewQueryModal depends on ownState; a fresh
|
||||
// object on every render would refetch the query unnecessarily.
|
||||
const ownState = useMemo(
|
||||
() =>
|
||||
createOwnStateWithChartState(
|
||||
(dataMaskOwnState as JsonObject) || EMPTY_OBJECT,
|
||||
{
|
||||
state:
|
||||
getChartStateWithFallback(
|
||||
chartState as { state?: JsonObject } | undefined,
|
||||
formData as JsonObject,
|
||||
sliceVizType,
|
||||
) ?? undefined,
|
||||
},
|
||||
sliceVizType,
|
||||
),
|
||||
[dataMaskOwnState, chartState, formData, sliceVizType],
|
||||
);
|
||||
|
||||
const exportTable = useCallback(
|
||||
async (format: string, isFullCSV: boolean, isPivot = false) => {
|
||||
const logAction =
|
||||
@@ -727,6 +747,7 @@ const Chart = (props: ChartProps) => {
|
||||
height={getHeaderHeight()}
|
||||
exportPivotExcel={exportPivotExcel as unknown as (arg0: string) => void}
|
||||
chartHolderRef={props.chartHolderRef}
|
||||
ownState={ownState}
|
||||
/>
|
||||
|
||||
{/*
|
||||
@@ -777,18 +798,7 @@ const Chart = (props: ChartProps) => {
|
||||
formData={
|
||||
formData as unknown as import('@superset-ui/core').QueryFormData
|
||||
}
|
||||
ownState={createOwnStateWithChartState(
|
||||
(dataMask[props.id]?.ownState as JsonObject) || EMPTY_OBJECT,
|
||||
{
|
||||
state:
|
||||
getChartStateWithFallback(
|
||||
chartState as { state?: JsonObject } | undefined,
|
||||
formData as JsonObject,
|
||||
slice.viz_type,
|
||||
) ?? undefined,
|
||||
},
|
||||
slice.viz_type,
|
||||
)}
|
||||
ownState={ownState}
|
||||
queriesResponse={chart.queriesResponse ?? null}
|
||||
timeout={timeout}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
import { screen, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as chartAction from 'src/components/Chart/chartAction';
|
||||
import type { ChartDataRequestResponse } from 'src/components/Chart/chartAction';
|
||||
import ViewQueryModal from './ViewQueryModal';
|
||||
|
||||
const mockFormData = {
|
||||
@@ -26,9 +28,19 @@ const mockFormData = {
|
||||
viz_type: 'table',
|
||||
};
|
||||
|
||||
// Minimal, type-correct response that satisfies ChartDataRequestResponse.
|
||||
// A real Response instance avoids the 16 required Response fields that an
|
||||
// empty object ({}) fails to overlap. The assertions only inspect the call
|
||||
// arguments, never the resolved value's contents.
|
||||
const mockChartDataResponse: ChartDataRequestResponse = {
|
||||
response: new Response(),
|
||||
json: { result: [] },
|
||||
};
|
||||
|
||||
const chartDataEndpoint = 'glob:*/api/v1/chart/data*';
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.resetAllMocks();
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
});
|
||||
@@ -117,3 +129,91 @@ test('renders both Alert and SQL query when parsing error occurs', async () => {
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
});
|
||||
|
||||
test('passes ownState through to getChartDataRequest', async () => {
|
||||
/**
|
||||
* Regression test for PR #35208 - the ViewQueryModal must forward the
|
||||
* chart's ownState (e.g. table search text, order_by) to the data request
|
||||
* so that the displayed SQL reflects the same filters applied to the chart.
|
||||
*/
|
||||
const getChartDataRequestSpy = jest
|
||||
.spyOn(chartAction, 'getChartDataRequest')
|
||||
.mockResolvedValue(mockChartDataResponse);
|
||||
|
||||
const ownState = { searchText: 'foo', order_by: [['col', 'asc']] };
|
||||
|
||||
render(
|
||||
<ViewQueryModal latestQueryFormData={mockFormData} ownState={ownState} />,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getChartDataRequestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(getChartDataRequestSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: mockFormData,
|
||||
ownState,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('strips clientView from ownState before the query request', async () => {
|
||||
/**
|
||||
* clientView holds the full client-side row/column snapshot (added by
|
||||
* TableChart) and is irrelevant to SQL generation. It must be stripped
|
||||
* before the request - matching ExploreViewContainer and Dashboard - to
|
||||
* avoid bloating the payload (or triggering 413) on large tables.
|
||||
*/
|
||||
const getChartDataRequestSpy = jest
|
||||
.spyOn(chartAction, 'getChartDataRequest')
|
||||
.mockResolvedValue(mockChartDataResponse);
|
||||
|
||||
const ownState = {
|
||||
searchText: 'foo',
|
||||
// Simulate a large client-side snapshot that TableChart writes
|
||||
clientView: { rows: [{ a: 1 }, { a: 2 }], columns: ['a'] },
|
||||
};
|
||||
|
||||
render(
|
||||
<ViewQueryModal latestQueryFormData={mockFormData} ownState={ownState} />,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getChartDataRequestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const calledOwnState = getChartDataRequestSpy.mock.calls[0][0].ownState;
|
||||
expect(calledOwnState).not.toHaveProperty('clientView');
|
||||
expect(calledOwnState).toEqual(
|
||||
expect.objectContaining({ searchText: 'foo' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to empty ownState when prop is omitted', async () => {
|
||||
/**
|
||||
* Covers the `ownState || {}` fallback branch in ViewQueryModal - when no
|
||||
* ownState is provided, the data request must still be called with an empty
|
||||
* object rather than undefined, matching getChartDataRequest's contract.
|
||||
*/
|
||||
const getChartDataRequestSpy = jest
|
||||
.spyOn(chartAction, 'getChartDataRequest')
|
||||
.mockResolvedValue(mockChartDataResponse);
|
||||
|
||||
render(<ViewQueryModal latestQueryFormData={mockFormData} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getChartDataRequestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(getChartDataRequestSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: mockFormData,
|
||||
ownState: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, Fragment, useEffect, useState } from 'react';
|
||||
import { FC, Fragment, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ensureIsArray,
|
||||
getClientErrorObject,
|
||||
JsonObject,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
@@ -33,6 +35,7 @@ import ViewQuery from 'src/explore/components/controls/ViewQuery';
|
||||
|
||||
interface Props {
|
||||
latestQueryFormData: QueryFormData;
|
||||
ownState?: JsonObject;
|
||||
}
|
||||
|
||||
type Result = {
|
||||
@@ -48,38 +51,47 @@ const ViewQueryModalContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
`;
|
||||
|
||||
const ViewQueryModal: FC<Props> = ({ latestQueryFormData }) => {
|
||||
const ViewQueryModal: FC<Props> = ({ latestQueryFormData, ownState }) => {
|
||||
const [result, setResult] = useState<Result[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadChartData = (resultType: string) => {
|
||||
setIsLoading(true);
|
||||
getChartDataRequest({
|
||||
formData: latestQueryFormData,
|
||||
resultFormat: 'json',
|
||||
resultType,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setResult(ensureIsArray(json.result) as Result[]);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
const loadChartData = useCallback(
|
||||
(resultType: string) => {
|
||||
setIsLoading(true);
|
||||
// Strip clientView (client-side row/column snapshot) from ownState before
|
||||
// requesting the query, matching the chart query path in ExploreViewContainer
|
||||
// and Dashboard's activeAllDashboardFilters. clientView is irrelevant to SQL
|
||||
// generation and can bloat the payload (or trigger 413) on large tables.
|
||||
const ownStateForQuery = omit(ownState, ['clientView']) || {};
|
||||
getChartDataRequest({
|
||||
formData: latestQueryFormData,
|
||||
resultFormat: 'json',
|
||||
resultType,
|
||||
ownState: ownStateForQuery,
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setError(
|
||||
error ||
|
||||
message ||
|
||||
response.statusText ||
|
||||
t('Sorry, An error occurred'),
|
||||
);
|
||||
.then(({ json }) => {
|
||||
setResult(ensureIsArray(json.result) as Result[]);
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
})
|
||||
.catch(response => {
|
||||
getClientErrorObject(response).then(({ error, message }) => {
|
||||
setError(
|
||||
error ||
|
||||
message ||
|
||||
response.statusText ||
|
||||
t('Sorry, An error occurred'),
|
||||
);
|
||||
setIsLoading(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
},
|
||||
[latestQueryFormData, ownState],
|
||||
);
|
||||
useEffect(() => {
|
||||
loadChartData('query');
|
||||
}, [JSON.stringify(latestQueryFormData)]);
|
||||
}, [loadChartData]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
|
||||
@@ -1067,6 +1067,7 @@ export const useExploreAdditionalActionsMenu = (
|
||||
modalBody={
|
||||
<ViewQueryModal
|
||||
latestQueryFormData={latestQueryFormData as QueryFormData}
|
||||
ownState={ownState}
|
||||
/>
|
||||
}
|
||||
draggable
|
||||
|
||||
Reference in New Issue
Block a user