Compare commits

..

27 Commits

Author SHA1 Message Date
Beto Dealmeida
0ade0915d0 Extension loading 2026-01-28 11:45:03 -05:00
Beto Dealmeida
3596ef304e More extensions 2026-01-27 15:18:52 -05:00
Beto Dealmeida
acb8b63023 Make extension 2026-01-27 12:10:53 -05:00
Beto Dealmeida
b9ab0ced77 Fix order 2026-01-26 18:47:44 -05:00
Beto Dealmeida
bfbb68c3c8 WIP 2025-12-15 10:26:52 -05:00
Beto Dealmeida
b437421a8e Fix filter 2025-12-11 15:06:13 -05:00
Beto Dealmeida
e253bd2fb3 Fix mapping 2025-12-11 10:58:17 -05:00
Beto Dealmeida
bfb7048e42 Frontend 2025-12-11 10:32:06 -05:00
Beto Dealmeida
2833b69ca0 WIP 2025-12-10 16:26:45 -05:00
Beto Dealmeida
6e17714a19 WIP 2025-12-10 13:54:19 -05:00
Beto Dealmeida
8a0aaa42ec feat: semantic layer implementation (Snowflake) 2025-12-05 15:32:01 -05:00
Beto Dealmeida
af479a9d99 More cleanup 2025-12-03 10:30:38 -05:00
Beto Dealmeida
77f60f42e6 More cleanup 2025-12-02 12:13:49 -05:00
Beto Dealmeida
f0121a166e chore: improve types 2025-12-01 17:33:11 -05:00
Beto Dealmeida
0c4b0cb9b9 Cleanup code 2025-12-01 11:16:06 -05:00
Beto Dealmeida
a36bbf8ffd Small fixes 2025-11-26 16:15:08 -05:00
Beto Dealmeida
99525c1ce9 Fix errors 2025-11-26 13:21:48 -05:00
Beto Dealmeida
889e9bbade Fix lint/tests 2025-11-25 16:59:17 -05:00
Beto Dealmeida
b809a990ee Fix pylint 2025-11-25 11:50:04 -05:00
Beto Dealmeida
9c7fcbf548 Fix tests 2025-11-25 11:48:46 -05:00
Beto Dealmeida
046aabee73 Fix lint 2025-11-24 16:51:54 -05:00
Beto Dealmeida
b672c7b853 Remove AI artifacts 2025-11-24 14:41:30 -05:00
Beto Dealmeida
ea33d797a7 feat: explorable protocol 2025-11-24 14:39:25 -05:00
Enzo Martellucci
ab8352ee66 fix: Table chart types headers are offset from the columns in the table (#36190)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2025-11-24 10:25:55 -08:00
JUST.in DO IT
bf2cef7d87 chore(sqllab): add logging for switching south panel tabs (#36168) 2025-11-24 10:23:55 -08:00
dependabot[bot]
a6b6eb4ab3 chore(deps-dev): bump @types/lodash from 4.17.20 to 4.17.21 in /superset-websocket (#36231)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 10:21:17 -08:00
Enzo Martellucci
cac6ffcd3c fix: Extra controls width for Area Chart on dashboards (#36133)
Co-authored-by: Diego Pucci <diegopucci.me@gmail.com>
2025-11-24 16:57:17 +02:00
206 changed files with 27768 additions and 7602 deletions

View File

@@ -53,7 +53,7 @@ extension-pkg-whitelist=pyarrow
[MESSAGES CONTROL]
disable=all
enable=disallowed-json-import,disallowed-sql-import,consider-using-transaction
enable=json-import,disallowed-sql-import,consider-using-transaction
[REPORTS]

View File

@@ -67,6 +67,7 @@ x-superset-volumes: &superset-volumes
- ./superset-frontend:/app/superset-frontend
- superset_home_light:/app/superset_home
- ./tests:/app/tests
- ./extensions:/app/extensions
x-common-build: &common-build
context: .
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`

View File

@@ -105,7 +105,15 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True}
# Extensions configuration
# For local development, point to the extensions directory
# Note: If running in Docker, this path needs to be accessible from inside the container
EXTENSIONS_PATH = os.getenv("EXTENSIONS_PATH", "/app/extensions")
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"ENABLE_EXTENSIONS": True,
}
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -0,0 +1,5 @@
# Requirements for the Snowflake Semantic Layer extension
# Install with: pip install -r extensions/requirements-snowflake.txt
snowflake-connector-python>=3.0.0
snowflake-sqlalchemy>=1.5.0

View File

@@ -54,6 +54,7 @@ module.exports = {
['@babel/plugin-transform-runtime', { corejs: 3 }],
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
['babel-plugin-typescript-to-proptypes', { loose: true }],
'react-hot-loader/babel',
[
'@emotion/babel-plugin',
{

File diff suppressed because it is too large Load Diff

View File

@@ -138,7 +138,7 @@
"@visx/xychart": "^3.5.1",
"ag-grid-community": "34.2.0",
"ag-grid-react": "34.2.0",
"antd": "^5.26.3",
"antd": "^5.26.0",
"chrono-node": "^2.7.8",
"classnames": "^2.2.5",
"content-disposition": "^0.5.4",
@@ -179,17 +179,15 @@
"ol": "^7.5.2",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"query-string": "^7.1.3",
"re-resizable": "^6.10.1",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^16.0.1",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-dom": "^17.0.2",
"react-google-recaptcha": "^3.1.0",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.20.0",
"react-lines-ellipsis": "^0.16.1",
@@ -239,8 +237,10 @@
"@babel/runtime": "^7.28.4",
"@babel/runtime-corejs3": "^7.28.2",
"@babel/types": "^7.26.9",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.13.0",
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.56.0",
@@ -256,11 +256,12 @@
"@storybook/react-webpack5": "8.1.11",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.14.0",
"@swc/plugin-emotion": "^13.1.0",
"@swc/plugin-transform-imports": "^11.1.0",
"@swc/plugin-emotion": "^12.0.0",
"@swc/plugin-transform-imports": "^10.0.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
"@types/dom-to-image": "^2.6.7",
@@ -270,9 +271,8 @@
"@types/math-expression-evaluator": "^1.3.3",
"@types/mousetrap": "^1.6.15",
"@types/node": "^24.8.1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-gravatar": "^2.6.14",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-json-tree": "^0.13.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
@@ -378,10 +378,6 @@
"npm": "^10.8.1"
},
"overrides": {
"react-sortable-hoc": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"core-js": "^3.38.1",
"d3-color": "^3.1.0",
"puppeteer": "^22.4.1",

View File

@@ -23,7 +23,8 @@
"@types/lodash": "^4.17.20",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
@@ -31,12 +32,12 @@
"@types/tinycolor2": "*"
},
"peerDependencies": {
"react": "^18.2.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"nanoid": "^5.0.9",
"react-dom": "^18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"@fontsource/fira-code": "^5.2.6",

View File

@@ -35,14 +35,15 @@
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^18.2.0",
"react": "^17.0.2",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -97,15 +97,16 @@
"@emotion/styled": "^11.14.1",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/react": "*",
"@types/react-loadable": "*",
"@types/react-window": "^1.8.8",
"@types/tinycolor2": "*",
"nanoid": "^5.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useJsonValidation } from './useJsonValidation';
describe('useJsonValidation', () => {

View File

@@ -19,12 +19,12 @@
import {
useEffect,
useState,
RefObject,
forwardRef,
ComponentType,
ForwardRefExoticComponent,
PropsWithoutRef,
RefAttributes,
ForwardedRef,
} from 'react';
import { Loading } from '../Loading';
@@ -54,7 +54,7 @@ function DefaultPlaceholder({
* first (if provided) and re-render once import is complete.
*/
export function AsyncEsmComponent<
P = Record<string, unknown>,
P = PlaceholderProps,
M = ComponentType<P> | { default: ComponentType<P> },
>(
/**
@@ -98,8 +98,8 @@ export function AsyncEsmComponent<
};
const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent(
props: PropsWithoutRef<FullProps>,
ref: ForwardedRef<ComponentType<FullProps>>,
props: FullProps,
ref: RefObject<ComponentType<FullProps>>,
) {
const [loaded, setLoaded] = useState(component !== undefined);
useEffect(() => {

View File

@@ -24,6 +24,7 @@ import type {
ButtonVariantType,
ButtonColorType,
} from 'antd/es/button';
import { IconType } from '@superset-ui/core/components/Icons/types';
import type { TooltipPlacement } from '../Tooltip/types';
export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
@@ -48,4 +49,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
buttonStyle?: ButtonStyle;
cta?: boolean;
showMarginRight?: boolean;
icon?: IconType;
};

View File

@@ -65,7 +65,7 @@ export const Component = (props: DropdownContainerProps) => {
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
const containerRef = useRef<DropdownRef>(null);
const onOverflowingStateChange = useCallback(
(value: OverflowingState) => {
value => {
if (!isEqual(overflowingState, value)) {
setItems(generateItems(value));
setOverflowingState(value);

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import type { CSSProperties, ReactElement, RefObject, ReactNode } from 'react';
import { IconType } from '../Icons';
/**
* Container item.
@@ -69,7 +70,7 @@ export interface DropdownContainerProps {
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactNode;
dropdownTriggerIcon?: IconType;
/**
* Text of the dropdown trigger.
*/

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import type { ReactNode, SyntheticEvent } from 'react';
import type { IconType } from '@superset-ui/core/components';
export type EmptyStateSize = 'small' | 'medium' | 'large';
@@ -25,7 +26,7 @@ export type EmptyStateProps = {
description?: ReactNode;
image?: ReactNode | string;
buttonText?: ReactNode;
buttonIcon?: ReactNode;
buttonIcon?: IconType;
buttonAction?: (event: SyntheticEvent) => void;
size?: EmptyStateSize;
children?: ReactNode;

View File

@@ -16,4 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
export { Form } from 'antd';
import { Form as AntdForm } from 'antd';
import { FormProps } from './types';
function CustomForm(props: FormProps) {
return <AntdForm {...props} />;
}
export const Form = Object.assign(CustomForm, {
useForm: AntdForm.useForm,
Item: AntdForm.Item,
List: AntdForm.List,
ErrorList: AntdForm.ErrorList,
Provider: AntdForm.Provider,
});

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
export type { FormProps, FormInstance, FormItemProps } from 'antd';
export type { FormProps, FormInstance, FormItemProps } from 'antd/es/form';
export interface LabeledErrorBoundInputProps {
label?: string;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FC, PropsWithChildren } from 'react';
import { FC } from 'react';
import { styled, useTheme, css } from '@apache-superset/core/ui';
import { Skeleton } from '../Skeleton';
import { Card } from '../Card';
@@ -134,7 +134,7 @@ const ThinSkeleton = styled(Skeleton)`
const paragraphConfig = { rows: 1, width: 150 };
const AnchorLink: FC<PropsWithChildren<LinkProps>> = ({ to, children }) => (
const AnchorLink: FC<LinkProps> = ({ to, children }) => (
<a href={to}>{children}</a>
);

View File

@@ -16,12 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import type {
ReactNode,
ComponentType,
ReactElement,
PropsWithChildren,
} from 'react';
import type { ReactNode, ComponentType, ReactElement } from 'react';
import type { BackgroundPosition } from './ImageLoader';
export interface LinkProps {
@@ -32,7 +27,7 @@ export interface ListViewCardProps {
title?: ReactNode;
subtitle?: ReactNode;
url?: string;
linkComponent?: ComponentType<PropsWithChildren<LinkProps>>;
linkComponent?: ComponentType<LinkProps>;
imgURL?: string | null;
imgFallbackURL?: string;
imgPosition?: BackgroundPosition;

View File

@@ -194,7 +194,7 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
}
const onResize = useCallback(
(width: number) => {
width => {
// Calculates the breakpoint width to collapse the bar.
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
const breakpoint =

View File

@@ -54,7 +54,7 @@ export function FormModal({
}, [onSave, resetForm]);
const handleFormSubmit = useCallback(
async (values: Object) => {
async values => {
try {
setIsSaving(true);
await formSubmitHandler(values);
@@ -113,7 +113,7 @@ export function FormModal({
onValuesChange={onFormChange}
onFieldsChange={onFormChange}
>
{children}
{typeof children === 'function' ? children(form) : children}
</Form>
</Modal>
);

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { render, screen, fireEvent } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { TableInstance, useTable } from 'react-table';
import TableCollection from '.';

View File

@@ -33,7 +33,6 @@ import {
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
} from 'react-table';
import { ColumnsType } from 'antd/es/table';
import { SortOrder } from '../Table';
@@ -88,11 +87,11 @@ export function mapColumns<T extends object>(
columns: EnhancedColumnInstance<T>[],
headerGroups: EnhancedHeaderGroup<T>[],
columnsForWrapText?: string[],
): ColumnsType<object> {
) {
return columns.map(column => {
const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id);
return {
title: column.Header as ReactNode,
title: column.Header,
dataIndex: column.id?.includes('.') ? column.id.split('.') : column.id,
hidden: column.hidden,
key: column.id,
@@ -122,7 +121,7 @@ export function mapColumns<T extends object>(
column,
});
}
return String(val);
return val;
},
className: column.className,
};

View File

@@ -96,8 +96,8 @@ const StyledPlus = styled.span`
export default function TruncatedList<ListItemType>({
items,
renderVisibleItem = item => item as ReactNode,
renderTooltipItem = item => item as ReactNode,
renderVisibleItem = item => item,
renderTooltipItem = item => item,
getKey = item => item as unknown as Key,
maxLinks = 20,
}: TruncatedListProps<ListItemType>) {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useChangeEffect } from './useChangeEffect';
test('call callback the first time with undefined and value', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useComponentDidMount } from './useComponentDidMount';
test('the effect should only be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useComponentDidUpdate } from './useComponentDidUpdate';
test('the effect should not be executed on the first render', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useElementOnScreen } from './useElementOnScreen';
const observeMock = jest.fn();

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { usePrevious } from './usePrevious';
test('get undefined on the first render when initialValue is not defined', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import useCSSTextTruncation from './useCSSTextTruncation';
afterEach(() => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { RefObject } from 'react';
import useChildElementTruncation from './useChildElementTruncation';

View File

@@ -19,16 +19,27 @@
import { DatasourceType } from './types/Datasource';
const DATASOURCE_TYPE_MAP: Record<string, DatasourceType> = {
table: DatasourceType.Table,
query: DatasourceType.Query,
dataset: DatasourceType.Dataset,
sl_table: DatasourceType.SlTable,
saved_query: DatasourceType.SavedQuery,
semantic_view: DatasourceType.SemanticView,
};
export default class DatasourceKey {
readonly id: number;
readonly id: number | string;
readonly type: DatasourceType;
constructor(key: string) {
const [idStr, typeStr] = key.split('__');
this.id = parseInt(idStr, 10);
this.type = DatasourceType.Table; // default to SqlaTable model
this.type = typeStr === 'query' ? DatasourceType.Query : this.type;
// Only parse as integer if the entire string is numeric
// (parseInt would incorrectly parse "85d3139f..." as 85)
const isNumeric = /^\d+$/.test(idStr);
this.id = isNumeric ? parseInt(idStr, 10) : idStr;
this.type = DATASOURCE_TYPE_MAP[typeStr] ?? DatasourceType.Table;
}
public toString() {

View File

@@ -26,6 +26,7 @@ export enum DatasourceType {
Dataset = 'dataset',
SlTable = 'sl_table',
SavedQuery = 'saved_query',
SemanticView = 'semantic_view',
}
export interface Currency {
@@ -37,7 +38,7 @@ export interface Currency {
* Datasource metadata.
*/
export interface Datasource {
id: number;
id: number | string;
name: string;
type: DatasourceType;
columns: Column[];

View File

@@ -156,7 +156,7 @@ export interface QueryObject
export interface QueryContext {
datasource: {
id: number;
id: number | string;
type: DatasourceType;
};
/** Force refresh of all queries */

View File

@@ -21,7 +21,6 @@ import {
MouseEventHandler,
ReactElement,
ComponentType,
PropsWithChildren,
} from 'react';
import type { Editor } from 'brace';
import type { QueryData } from '../chart/types/QueryResponse';
@@ -250,7 +249,7 @@ export type Extensions = Partial<{
'navbar.right-menu.item.icon': ComponentType<RightMenuItemIconProps>;
'navbar.right': ComponentType;
'report-modal.dropdown.item.icon': ComponentType;
'root.context.provider': ComponentType<PropsWithChildren>;
'root.context.provider': ComponentType;
'welcome.message': ComponentType;
'welcome.banner': ComponentType;
'welcome.main.replacement': ComponentType;

View File

@@ -46,8 +46,8 @@
"gh-pages": "^6.3.0",
"jquery": "^3.7.1",
"memoize-one": "^5.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-loadable": "^5.5.0",
"react-resizable": "^3.0.5"
},

View File

@@ -32,8 +32,8 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -33,7 +33,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
}
}

View File

@@ -30,8 +30,8 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -36,7 +36,7 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"mapbox-gl": "*",
"react": "^18.2.0"
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -30,8 +30,8 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -35,7 +35,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
}
}

View File

@@ -32,9 +32,9 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"@testing-library/react": "^12.1.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -31,8 +31,8 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -38,7 +38,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
}
}

View File

@@ -64,8 +64,8 @@
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"mapbox-gl": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-map-gl": "^6.1.19"
},
"publishConfig": {

View File

@@ -196,5 +196,5 @@ export const DeckGLContainerStyledWrapper = styled(DeckGLContainer)`
`;
export type DeckGLContainerHandle = typeof DeckGLContainer & {
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
setTooltip: (tooltip: ReactNode) => void;
};

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import React from 'react';
import { PickingInfo } from '@deck.gl/core';
import { JsonObject, QueryFormData } from '@superset-ui/core';
import {
@@ -99,9 +98,9 @@ describe('getAggFunc', () => {
describe('commonLayerProps', () => {
const mockSetTooltip = jest.fn();
const mockSetTooltipContent = jest
.fn()
.mockReturnValue((o: JsonObject) => `Tooltip for ${o}` as React.ReactNode);
const mockSetTooltipContent = jest.fn(
() => (o: JsonObject) => `Tooltip for ${o}`,
);
const mockOnSelect = jest.fn();
it('returns correct props when js_tooltip is provided', () => {

View File

@@ -42,7 +42,7 @@
"peerDependencies": {
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"@apache-superset/core": "*"
"@apache-superset/core": "*",
"react": "^17.0.2"
}
}

View File

@@ -42,13 +42,14 @@
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -37,8 +37,10 @@ import {
type ColDef,
type ColumnState,
ModuleRegistry,
GridState,
GridReadyEvent,
GridState,
CellClickedEvent,
IMenuActionParams,
} from '@superset-ui/core/components/ThemedAgGridReact';
import {
AgGridChartState,
@@ -72,7 +74,7 @@ export interface AgGridTableProps {
gridHeight?: number;
updateInterval?: number;
data?: any[];
onGridReady?: (params: any) => void;
onGridReady?: (params: GridReadyEvent) => void;
colDefsFromProps: any[];
includeSearch: boolean;
allowRearrangeColumns: boolean;
@@ -91,7 +93,7 @@ export interface AgGridTableProps {
percentMetrics: string[];
serverPageLength: number;
hasServerPageLengthChanged: boolean;
handleCrossFilter: (event: any) => void;
handleCrossFilter: (event: CellClickedEvent | IMenuActionParams) => void;
isActiveFilterValue: (key: string, val: DataRecordValue) => boolean;
renderTimeComparisonDropdown: () => JSX.Element | null;
cleanedTotals: DataRecord;
@@ -277,7 +279,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
};
const handleColumnHeaderClick = useCallback(
(params: any) => {
params => {
const colId = params?.column?.colId;
const sortDir = params?.column?.sort;
handleColSort(colId, sortDir);
@@ -421,8 +423,8 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
rowData={rowData}
headerHeight={36}
rowHeight={30}
columnDefs={colDefsFromProps as any}
defaultColDef={defaultColDef as any}
columnDefs={colDefsFromProps}
defaultColDef={defaultColDef}
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
rowSelection="multiple"
animateRows

View File

@@ -172,7 +172,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
);
const timestampFormatter = useCallback(
(value: any) => getTimeFormatterForGranularity(timeGrain)(value),
value => getTimeFormatterForGranularity(timeGrain)(value),
[timeGrain],
);

View File

@@ -67,5 +67,5 @@ export const TextCellRenderer = (params: CellRendererProps) => {
}
}
return <div>{String(valueFormatted ?? value)}</div>;
return <div>{valueFormatted ?? value}</div>;
};

View File

@@ -47,7 +47,7 @@
"geostyler-wfs-parser": "^2.0.0",
"ol": "^7.1.0",
"polished": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

View File

@@ -19,7 +19,7 @@
import Layer from 'ol/layer/Layer';
import { FrameState } from 'ol/Map';
import { apply as applyTransform } from 'ol/transform';
import { createRoot } from 'react-dom/client';
import ReactDOM from 'react-dom';
import { SupersetTheme } from '@apache-superset/core/ui';
import { ChartConfig, ChartLayerOptions, ChartSizeValues } from '../types';
import { createChartComponent } from '../util/chartUtil';
@@ -33,8 +33,6 @@ import Loader from '../images/loading.gif';
export class ChartLayer extends Layer {
charts: any[] = [];
chartRoots: Map<HTMLElement, any> = new Map();
chartConfigs: ChartConfig = {
type: 'FeatureCollection',
features: [],
@@ -168,11 +166,7 @@ export class ChartLayer extends Layer {
*/
removeAllChartElements() {
this.charts.forEach(chart => {
const root = this.chartRoots.get(chart.htmlElement);
if (root) {
root.unmount();
this.chartRoots.delete(chart.htmlElement);
}
ReactDOM.unmountComponentAtNode(chart.htmlElement);
chart.htmlElement.remove();
});
this.charts = [];
@@ -197,9 +191,7 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
const root = createRoot(container);
this.chartRoots.set(container, root);
root.render(chartComponent);
ReactDOM.render(chartComponent, container);
return {
htmlElement: container,
@@ -235,10 +227,7 @@ export class ChartLayer extends Layer {
this.theme,
this.locale,
);
const root = this.chartRoots.get(chart.htmlElement);
if (root) {
root.render(chartComponent);
}
ReactDOM.render(chartComponent, chart.htmlElement);
return {
...chart,

View File

@@ -35,7 +35,7 @@
"@superset-ui/core": "*",
"echarts": "*",
"memoize-one": "*",
"react": "^18.2.0"
"react": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -57,7 +57,7 @@ export default function EchartsMixedTimeseries({
);
const getCrossFilterDataMask = useCallback(
(seriesName: any, seriesIndex: any) => {
(seriesName, seriesIndex) => {
const selected: string[] = Object.values(selectedValues || {});
let values: string[];
if (selected.includes(seriesName)) {

View File

@@ -0,0 +1,311 @@
/**
* 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,
waitFor,
cleanup,
} from '../../../../spec/helpers/testing-library';
import { AxisType } from '@superset-ui/core';
import type { EChartsCoreOption } from 'echarts/core';
import type { ReactNode } from 'react';
import {
LegendOrientation,
LegendType,
type EchartsHandler,
type EchartsProps,
} from '../types';
import EchartsTimeseries from './EchartsTimeseries';
import {
EchartsTimeseriesSeriesType,
OrientationType,
type EchartsTimeseriesFormData,
type TimeseriesChartTransformedProps,
} from './types';
const mockEchart = jest.fn();
jest.mock('../components/Echart', () => {
const { forwardRef } = jest.requireActual<typeof import('react')>('react');
const MockEchart = forwardRef<EchartsHandler | null, EchartsProps>(
(props, ref) => {
mockEchart(props);
void ref;
return null;
},
);
MockEchart.displayName = 'MockEchart';
return {
__esModule: true,
default: MockEchart,
};
});
jest.mock('../components/ExtraControls', () => ({
ExtraControls: ({ children }: { children?: ReactNode }) => (
<div data-testid="extra-controls">{children}</div>
),
}));
const originalResizeObserver = globalThis.ResizeObserver;
const offsetHeightDescriptor = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'offsetHeight',
);
let mockOffsetHeight = 0;
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
configurable: true,
get() {
return mockOffsetHeight;
},
});
});
afterAll(() => {
if (offsetHeightDescriptor) {
Object.defineProperty(
HTMLElement.prototype,
'offsetHeight',
offsetHeightDescriptor,
);
} else {
delete (HTMLElement.prototype as { offsetHeight?: number }).offsetHeight;
}
});
afterEach(() => {
cleanup();
mockEchart.mockReset();
(globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver =
originalResizeObserver;
});
const defaultFormData: EchartsTimeseriesFormData & {
vizType: string;
dateFormat: string;
numberFormat: string;
granularitySqla?: string;
} = {
annotationLayers: [],
area: false,
colorScheme: undefined,
timeShiftColor: false,
contributionMode: undefined,
forecastEnabled: false,
forecastPeriods: 0,
forecastInterval: 0,
forecastSeasonalityDaily: null,
forecastSeasonalityWeekly: null,
forecastSeasonalityYearly: null,
logAxis: false,
markerEnabled: false,
markerSize: 1,
metrics: [],
minorSplitLine: false,
minorTicks: false,
opacity: 1,
orderDesc: false,
rowLimit: 0,
seriesType: EchartsTimeseriesSeriesType.Line,
stack: null,
stackDimension: '',
timeCompare: [],
tooltipTimeFormat: undefined,
showTooltipTotal: false,
showTooltipPercentage: false,
truncateXAxis: false,
truncateYAxis: false,
yAxisFormat: undefined,
xAxisForceCategorical: false,
xAxisTimeFormat: undefined,
timeGrainSqla: undefined,
forceMaxInterval: false,
xAxisBounds: [null, null],
yAxisBounds: [null, null],
zoomable: false,
richTooltip: false,
xAxisLabelRotation: 0,
xAxisLabelInterval: 0,
showValue: false,
onlyTotal: false,
showExtraControls: true,
percentageThreshold: 0,
orientation: OrientationType.Vertical,
datasource: '1__table',
viz_type: 'echarts_timeseries',
legendMargin: 0,
legendOrientation: LegendOrientation.Top,
legendType: LegendType.Plain,
showLegend: false,
legendSort: null,
xAxisTitle: '',
xAxisTitleMargin: 0,
yAxisTitle: '',
yAxisTitleMargin: 0,
yAxisTitlePosition: '',
time_range: 'No filter',
granularity: undefined,
granularity_sqla: undefined,
sql: '',
url_params: {},
custom_params: {},
extra_form_data: {},
adhoc_filters: [],
order_desc: false,
row_limit: 0,
row_offset: 0,
time_grain_sqla: undefined,
vizType: 'echarts_timeseries',
dateFormat: 'smart_date',
numberFormat: 'SMART_NUMBER',
};
const defaultProps: TimeseriesChartTransformedProps = {
echartOptions: {} as EChartsCoreOption,
formData: defaultFormData,
height: 400,
width: 800,
onContextMenu: jest.fn(),
setDataMask: jest.fn(),
onLegendStateChanged: jest.fn(),
refs: {},
emitCrossFilters: false,
coltypeMapping: {},
onLegendScroll: jest.fn(),
groupby: [],
labelMap: {},
setControlValue: jest.fn(),
selectedValues: {},
legendData: [],
xValueFormatter: String,
xAxis: {
label: 'x',
type: AxisType.Time,
},
onFocusedSeries: jest.fn(),
};
function getLatestHeight() {
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
return props.height;
}
test('observes extra control height changes when ResizeObserver is available', async () => {
const disconnectSpy = jest.fn();
const observeSpy = jest.fn();
class MockResizeObserver implements ResizeObserver {
private static latestInstance: MockResizeObserver | null = null;
private readonly callback: ResizeObserverCallback;
constructor(callback: ResizeObserverCallback) {
this.callback = callback;
MockResizeObserver.latestInstance = this;
}
observe = (target: Element) => {
observeSpy(target);
};
unobserve(_target: Element): void {
void _target;
}
disconnect = () => {
disconnectSpy();
};
trigger(entries: ResizeObserverEntry[] = []) {
this.callback(entries, this);
}
static getLatestInstance() {
return this.latestInstance;
}
}
(globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver =
MockResizeObserver as unknown as typeof ResizeObserver;
mockOffsetHeight = 42;
const { unmount } = render(<EchartsTimeseries {...defaultProps} />);
await waitFor(() => {
expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight);
});
expect(observeSpy).toHaveBeenCalledWith(expect.any(HTMLElement));
mockOffsetHeight = 24;
MockResizeObserver.getLatestInstance()?.trigger();
await waitFor(() => {
expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight);
});
expect(disconnectSpy).not.toHaveBeenCalled();
expect(MockResizeObserver.getLatestInstance()).not.toBeNull();
unmount();
expect(disconnectSpy).toHaveBeenCalled();
});
test('falls back to window resize listener when ResizeObserver is unavailable', async () => {
(globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver =
undefined;
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
mockOffsetHeight = 30;
const { unmount } = render(<EchartsTimeseries {...defaultProps} />);
await waitFor(() => {
expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight);
});
expect(addEventListenerSpy).toHaveBeenCalledWith(
'resize',
expect.any(Function),
);
mockOffsetHeight = 10;
window.dispatchEvent(new Event('resize'));
await waitFor(() => {
expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight);
});
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'resize',
expect.any(Function),
);
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});

View File

@@ -67,8 +67,32 @@ export default function EchartsTimeseries({
const extraControlRef = useRef<HTMLDivElement>(null);
const [extraControlHeight, setExtraControlHeight] = useState(0);
useEffect(() => {
const updatedHeight = extraControlRef.current?.offsetHeight || 0;
setExtraControlHeight(updatedHeight);
const element = extraControlRef.current;
if (!element) {
setExtraControlHeight(0);
return;
}
const updateHeight = () => {
setExtraControlHeight(element.offsetHeight || 0);
};
updateHeight();
if (typeof ResizeObserver === 'function') {
const resizeObserver = new ResizeObserver(() => {
updateHeight();
});
resizeObserver.observe(element);
return () => {
resizeObserver.disconnect();
};
}
window.addEventListener('resize', updateHeight);
return () => {
window.removeEventListener('resize', updateHeight);
};
}, [formData.showExtraControls]);
const hasDimensions = ensureIsArray(groupby).length > 0;

View File

@@ -46,7 +46,7 @@ export default function EchartsTreemap({
coltypeMapping,
}: TreemapTransformedProps) {
const getCrossFilterDataMask = useCallback(
(data: any, treePathInfo: any) => {
(data, treePathInfo) => {
if (data?.children) {
return undefined;
}
@@ -96,7 +96,7 @@ export default function EchartsTreemap({
);
const handleChange = useCallback(
(data: any, treePathInfo: any) => {
(data, treePathInfo) => {
if (!emitCrossFilters || groupby.length === 0) {
return;
}

View File

@@ -27,7 +27,6 @@
"access": "public"
},
"dependencies": {
"currencyformatter.js": "^1.0.5",
"handlebars-group-by": "^1.0.1",
"just-handlebars-helpers": "^1.0.19"
},
@@ -36,12 +35,12 @@
"@apache-superset/core": "*",
"@superset-ui/core": "*",
"ace-builds": "^1.4.14",
"dayjs": "^1.11.13",
"handlebars": "^4.7.8",
"lodash": "^4.17.11",
"react": "^18.2.0",
"dayjs": "^1.11.13",
"react": "^17.0.2",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/jest": "^30.0.0",

View File

@@ -70,7 +70,7 @@ ${helperDescriptions
<div>
<ControlHeader>
<div>
{props.label as any}
{props.label}
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={<SafeMarkdown source={helpersTooltipContent} />}

View File

@@ -47,7 +47,7 @@ const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
<div>
<ControlHeader>
<div>
{props.label as any}
{props.label}
<InfoTooltip
iconStyle={{ marginLeft: theme.sizeUnit }}
tooltip={t('You need to configure HTML sanitization to use CSS')}

View File

@@ -33,8 +33,8 @@
"@superset-ui/core": "*",
"lodash": "^4.17.11",
"prop-types": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@babel/types": "^7.28.4",

View File

@@ -42,13 +42,14 @@
"@superset-ui/core": "*",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "*",
"@testing-library/user-event": "*",
"@types/classnames": "*",
"@types/react": "*",
"match-sorter": "^6.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"publishConfig": {
"access": "public"

View File

@@ -166,7 +166,7 @@ function StickyWrap({
const scrollBodyRef = useRef<HTMLDivElement>(null); // main body
const scrollBarSize = getScrollBarSize();
const { bodyHeight, columnWidths } = sticky;
const { bodyHeight, columnWidths, hasVerticalScroll } = sticky;
const needSizer =
!columnWidths ||
sticky.width !== maxWidth ||
@@ -283,13 +283,18 @@ function StickyWrap({
</colgroup>
);
const headerContainerWidth = hasVerticalScroll
? maxWidth - scrollBarSize
: maxWidth;
headerTable = (
<div
key="header"
ref={scrollHeaderRef}
style={{
overflow: 'hidden',
scrollbarGutter: 'stable',
width: headerContainerWidth,
boxSizing: 'border-box',
}}
role="presentation"
>
@@ -309,7 +314,8 @@ function StickyWrap({
ref={scrollFooterRef}
style={{
overflow: 'hidden',
scrollbarGutter: 'stable',
width: headerContainerWidth,
boxSizing: 'border-box',
}}
role="presentation"
>
@@ -339,6 +345,8 @@ function StickyWrap({
height: bodyHeight,
overflow: 'auto',
scrollbarGutter: 'stable',
width: maxWidth,
boxSizing: 'border-box',
}}
css={scrollBarStyles}
onScroll={sticky.hasHorizontalScroll ? onScroll : undefined}

View File

@@ -347,7 +347,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
);
const timestampFormatter = useCallback(
(value: any) => getTimeFormatterForGranularity(timeGrain)(value),
value => getTimeFormatterForGranularity(timeGrain)(value),
[timeGrain],
);
const [tableSize, setTableSize] = useState<TableSize>({

View File

@@ -40,7 +40,7 @@
"@apache-superset/core": "*",
"@types/lodash": "*",
"@types/react": "*",
"react": "^18.2.0"
"react": "^17.0.2"
},
"devDependencies": {
"@types/d3-cloud": "^1.2.9"

View File

@@ -35,7 +35,8 @@ import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
import { ThemeController } from 'src/theme/ThemeController';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import reducerIndex from 'spec/helpers/reducerIndex';
import { QueryParamProvider } from 'use-query-params';
import { configureStore, Store } from '@reduxjs/toolkit';
@@ -98,17 +99,7 @@ export function createWrapper(options?: Options) {
}
if (useDnd) {
const DndWrapper = ({ children }: { children: ReactNode }) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
);
return <DndContext sensors={sensors}>{children}</DndContext>;
};
result = <DndWrapper>{result}</DndWrapper>;
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
}
if (useRedux || store) {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook, waitFor } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import { COMMON_ERR_MESSAGES } from '@superset-ui/core';
import {
createWrapper,
@@ -119,7 +119,7 @@ test('skips fetching validation if validator is undefined', () => {
});
test('returns validation if validator is configured', async () => {
const { result } = initialize(true);
const { result, waitFor } = initialize(true);
await waitFor(() =>
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1),
);
@@ -142,7 +142,7 @@ test('returns server error description', async () => {
},
{ overwriteRoutes: true },
);
const { result } = initialize(true);
const { result, waitFor } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([
@@ -166,7 +166,7 @@ test('returns session expire description when CSRF token expired', async () => {
},
{ overwriteRoutes: true },
);
const { result } = initialize(true);
const { result, waitFor } = initialize(true);
await waitFor(
() =>
expect(result.current.data).toEqual([

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
import { act, renderHook, waitFor } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import { getExtensionsRegistry } from '@superset-ui/core';
import {
createWrapper,
@@ -104,7 +104,7 @@ test('returns keywords including fetched function_names data', async () => {
const dbFunctionNamesApiRoute = `glob:*/api/v1/database/${expectDbId}/function_names/`;
fetchMock.get(dbFunctionNamesApiRoute, fakeFunctionNamesApiResult);
const { result } = renderHook(
const { result, waitFor } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',
@@ -240,7 +240,7 @@ test('returns column keywords among selected tables', async () => {
);
});
const { result } = renderHook(
const { result, waitFor } = renderHook(
() =>
useKeywords({
queryEditorId: expectQueryEditorId,
@@ -315,7 +315,7 @@ test('returns long keywords with docText', async () => {
),
);
});
const { result } = renderHook(
const { result, waitFor } = renderHook(
() =>
useKeywords({
queryEditorId: 'testqueryid',

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { FC, ReactNode } from 'react';
import { FC } from 'react';
import { t } from '@superset-ui/core';
import { styled, css } from '@apache-superset/core/ui';
import { ModalTrigger } from '@superset-ui/core/components';
@@ -92,7 +92,7 @@ const ShortcutCode = styled.code`
padding: ${({ theme }) => `${theme.sizeUnit}px ${theme.sizeUnit * 2}px`};
`;
const KeyboardShortcutButton: FC<{ children: ReactNode }> = ({ children }) => (
const KeyboardShortcutButton: FC<{}> = ({ children }) => (
<ModalTrigger
modalTitle={t('Keyboard shortcuts')}
modalBody={

View File

@@ -48,25 +48,14 @@ import { StaticPosition, StyledTooltip } from './styles';
interface QueryTableQuery
extends Omit<
QueryResponse,
| 'state'
| 'sql'
| 'progress'
| 'results'
| 'duration'
| 'started'
| 'user'
| 'db'
| 'querylink'
'state' | 'sql' | 'progress' | 'results' | 'duration' | 'started'
> {
state?: Record<string, any>;
sql?: ReactNode;
sql?: Record<string, any>;
progress?: Record<string, any>;
results?: Record<string, any>;
duration?: ReactNode;
started?: ReactNode;
user?: ReactNode;
db?: ReactNode;
querylink?: ReactNode;
}
interface QueryTableProps {
@@ -246,7 +235,7 @@ const QueryTable = ({
return queries
.map(query => {
const { state, sql, progress, ...rest } = query;
const q = { ...rest } as unknown as QueryTableQuery;
const q = rest as QueryTableQuery;
const status = statusAttributes[state] || statusAttributes.error;

View File

@@ -166,7 +166,7 @@ const RunQueryActionButton = ({
}
: {
buttonStyle: shouldShowStopBtn ? 'danger' : 'primary',
icon: icon as any,
icon,
})}
>
{text}

View File

@@ -31,6 +31,8 @@ import { SqlLabRootState } from 'src/SqlLab/types';
import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
import ExtensionsManager from 'src/extensions/ExtensionsManager';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import useLogAction from 'src/logger/useLogAction';
import { LOG_ACTIONS_SQLLAB_SWITCH_SOUTH_PANE_TAB } from 'src/logger/LogUtils';
import QueryHistory from '../QueryHistory';
import {
STATUS_OPTIONS,
@@ -126,11 +128,13 @@ const SouthPane = ({
[pinnedTables],
);
const southPaneRef = createRef<HTMLDivElement>();
const logAction = useLogAction({ sqlEditorId: queryEditorId });
const switchTab = (id: string) => {
dispatch(setActiveSouthPaneTab(id));
logAction(LOG_ACTIONS_SQLLAB_SWITCH_SOUTH_PANE_TAB, { tab: id });
};
const removeTable = useCallback(
(key: any, action: any) => {
(key, action) => {
if (action === 'remove') {
const table = pinnedTables.find(
({ dbId, catalog, schema, name }) =>

View File

@@ -580,7 +580,7 @@ const SqlEditor: FC<Props> = ({
};
const setQueryEditorAndSaveSql = useCallback(
(sql: any) => {
sql => {
dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
},
[dispatch, queryEditor],

View File

@@ -95,7 +95,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
new: isNewQuery,
...urlParams
} = {
...(this.context as any).requestedQuery,
...this.context.requestedQuery,
...bootstrapData.requested_query,
...queryParameters,
} as Record<string, string>;
@@ -135,7 +135,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
schema,
autorun,
sql,
isDataset: (this.context as any).isDataset,
isDataset: this.context.isDataset,
};
this.props.actions.addQueryEditor(newQueryEditor);
}

View File

@@ -19,7 +19,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { createWrapper } from 'spec/helpers/testing-library';
import useQueryEditor from '.';

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent, ErrorInfo } from 'react';
import { PureComponent } from 'react';
import {
ensureIsArray,
FeatureFlag,
@@ -205,13 +205,16 @@ class Chart extends PureComponent<ChartProps, {}> {
);
}
handleRenderContainerFailure(error: Error, info: ErrorInfo) {
handleRenderContainerFailure(
error: Error,
info: { componentStack: string } | null,
) {
const { actions, chartId } = this.props;
logging.warn(error);
actions.chartRenderingFailed(
error.toString(),
chartId,
info ? (info.componentStack ?? null) : null,
info ? info.componentStack : null,
);
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {

View File

@@ -18,7 +18,7 @@
*/
import { FeatureFlag, VizType } from '@superset-ui/core';
import { render, screen } from 'spec/helpers/testing-library';
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import mockState from 'spec/fixtures/mockState';
import { sliceId } from 'spec/fixtures/mockChartQueries';
import { noOp } from 'src/utils/common';

View File

@@ -1,274 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CSSProperties,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Menu } from '@superset-ui/core/components/Menu';
import {
BaseFormData,
Behavior,
Column,
ContextMenuFilters,
css,
ensureIsArray,
getChartMetadataRegistry,
t,
useTheme,
} from '@superset-ui/core';
import { Constants, Input, Loading } from '@superset-ui/core/components';
import { debounce } from 'lodash';
import { FixedSizeList as List } from 'react-window';
import { Icons } from '@superset-ui/core/components/Icons';
import { InputRef } from 'antd';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { getSubmenuYOffset } from '../utils';
import { VirtualizedMenuItem } from '../MenuItemWithTruncation';
import { Dataset } from '../types';
const SUBMENU_HEIGHT = 200;
const SHOW_COLUMNS_SEARCH_THRESHOLD = 10;
const SEARCH_INPUT_HEIGHT = 48;
export interface DrillByMenuItemsProps {
drillByConfig?: ContextMenuFilters['drillBy'];
formData: BaseFormData & { [key: string]: any };
contextMenuY?: number;
submenuIndex?: number;
onSelection?: (...args: any) => void;
onClick?: (event: MouseEvent) => void;
onCloseMenu?: () => void;
openNewModal?: boolean;
excludedColumns?: Column[];
open: boolean;
onDrillBy?: (column: Column, dataset: Dataset) => void;
dataset?: Dataset;
isLoadingDataset?: boolean;
}
export const DrillByMenuItems = ({
drillByConfig,
formData,
contextMenuY = 0,
submenuIndex = 0,
onSelection = () => {},
onClick = () => {},
onCloseMenu = () => {},
excludedColumns,
openNewModal = true,
open,
onDrillBy,
dataset,
isLoadingDataset = false,
...rest
}: DrillByMenuItemsProps) => {
const theme = useTheme();
const [searchInput, setSearchInput] = useState('');
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
const ref = useRef<InputRef>(null);
const columns = dataset ? ensureIsArray(dataset.drillable_columns) : [];
const showSearch = columns.length > SHOW_COLUMNS_SEARCH_THRESHOLD;
const handleSelection = useCallback(
(event: any, column: any) => {
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_enabled) {
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

@@ -90,11 +90,16 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
findPermission('can_explore', 'Superset', state.user?.roles),
);
const [datasource_id, datasource_type] = formData.datasource.split('__');
const [datasourceIdStr, datasource_type] = formData.datasource.split('__');
// Try to parse as integer, fall back to string (UUID) if NaN
const parsedDatasourceId = parseInt(datasourceIdStr, 10);
const datasource_id = Number.isNaN(parsedDatasourceId)
? datasourceIdStr
: parsedDatasourceId;
useEffect(() => {
// short circuit if the user is embedded as explore is not available
if (isEmbedded()) return;
postFormData(Number(datasource_id), datasource_type, formData, 0)
postFormData(datasource_id, datasource_type, formData, 0)
.then(key => {
setUrl(
`/explore/?form_data_key=${key}&dashboard_page_id=${dashboardPageId}`,

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import {
render,
screen,

View File

@@ -1,253 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
Dispatch,
ReactNode,
SetStateAction,
useCallback,
useMemo,
} from 'react';
import { isEmpty } from 'lodash';
import {
Behavior,
BinaryQueryObjectFilterClause,
css,
extractQueryFields,
getChartMetadataRegistry,
QueryFormData,
removeHTMLTags,
styled,
t,
} from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { Menu } from '@superset-ui/core/components/Menu';
import { RootState } from 'src/dashboard/types';
import { getSubmenuYOffset } from '../utils';
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
import { MenuItemWithTruncation } from '../MenuItemWithTruncation';
import { Dataset } from '../types';
const DRILL_TO_DETAIL = t('Drill to detail');
const DRILL_TO_DETAIL_BY = t('Drill to detail by');
const DISABLED_REASONS = {
DATABASE: t(
'Drill to detail is disabled for this database. Change the database settings to enable it.',
),
NO_AGGREGATIONS: t(
'Drill to detail is disabled because this chart does not group data by dimension value.',
),
NO_FILTERS: t(
'Right-click on a dimension value to drill to detail by that value.',
),
NOT_SUPPORTED: t(
'Drill to detail by value is not yet supported for this chart type.',
),
};
const DisabledMenuItem = ({
children,
menuKey,
...rest
}: {
children: ReactNode;
menuKey: string;
}) => (
<Menu.Item disabled key={menuKey} {...rest}>
<div
css={css`
white-space: normal;
max-width: 160px;
`}
>
{children}
</div>
</Menu.Item>
);
const Filter = ({
children,
stripHTML = false,
}: {
children: ReactNode;
stripHTML: boolean;
}) => {
const content =
stripHTML && typeof children === 'string'
? removeHTMLTags(children)
: children;
return <span>{content}</span>;
};
const StyledFilter = styled(Filter)`
${({ theme }) => `
font-weight: ${theme.fontWeightStrong};
color: ${theme.colorPrimary};
`}
`;
export type DrillDetailMenuItemsProps = {
formData: QueryFormData;
filters?: BinaryQueryObjectFilterClause[];
setFilters: Dispatch<SetStateAction<BinaryQueryObjectFilterClause[]>>;
isContextMenu?: boolean;
contextMenuY?: number;
onSelection?: () => void;
onClick?: (event: MouseEvent) => void;
submenuIndex?: number;
setShowModal: (show: boolean) => void;
key?: string;
forceSubmenuRender?: boolean;
dataset?: Dataset;
isLoadingDataset?: boolean;
};
const DrillDetailMenuItems = ({
formData,
filters = [],
isContextMenu = false,
contextMenuY = 0,
onSelection = () => null,
onClick = () => null,
submenuIndex = 0,
setFilters,
setShowModal,
key,
...props
}: DrillDetailMenuItemsProps) => {
const drillToDetailDisabled = useSelector<RootState, boolean | undefined>(
({ datasources }) =>
datasources[formData.datasource]?.database?.disable_drill_to_detail,
);
const openModal = useCallback(
(filters: any, event: any) => {
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

@@ -58,7 +58,7 @@ export default function TableControls({
);
const removeFilter = useCallback(
(colName: any) => {
colName => {
const updatedFilterMap = { ...filterMap };
delete updatedFilterMap[colName];
setFilters(Object.values(updatedFilterMap));
@@ -109,7 +109,7 @@ export default function TableControls({
>
{colName}
</span>
<strong data-test="filter-val">{String(val)}</strong>
<strong data-test="filter-val">{val}</strong>
</Tag>
))}
</div>

View File

@@ -116,7 +116,7 @@ export const useDrillDetailMenuItems = ({
);
const openModal = useCallback(
(filters: any, event: any) => {
(filters, event) => {
onClick(event);
onSelection();
setFilters(filters);

View File

@@ -52,7 +52,7 @@ export default function Field<V>({
errorMessage,
}: FieldProps<V>) {
const onControlChange = useCallback(
(newValue: any) => {
newValue => {
onChange(fieldKey, newValue);
},
[onChange, fieldKey],

View File

@@ -16,13 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
useContext,
useEffect,
useReducer,
createContext,
PropsWithChildren,
} from 'react';
import { useContext, useEffect, useReducer, createContext, FC } from 'react';
import {
ChartMetadata,
@@ -128,7 +122,7 @@ const sharedModules = {
'@superset-ui/core': () => import('@superset-ui/core'),
};
export const DynamicPluginProvider = ({ children }: PropsWithChildren) => {
export const DynamicPluginProvider: FC = ({ children }) => {
const [pluginState, dispatch] = useReducer(
pluginContextReducer,
dummyPluginContext,

View File

@@ -112,7 +112,7 @@ export const FilterableTable = ({
const keyword = useRef<string | undefined>(filterText);
keyword.current = filterText;
const keywordFilter = useCallback((node: any) => {
const keywordFilter = useCallback(node => {
if (keyword.current && node.data) {
return hasMatch(keyword.current, node.data);
}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useCellContentParser } from './useCellContentParser';
test('should return NULL for null cell data', () => {

View File

@@ -98,7 +98,7 @@ export const Header: React.FC<Params> = ({
const [currentSort, setCurrentSort] = useState<string | null>(null);
const [sortIndex, setSortIndex] = useState<number | null>();
const onSort = useCallback(
(event: any) => {
event => {
sortOption.current = (sortOption.current + 1) % SORT_DIRECTION.length;
const sort = SORT_DIRECTION[sortOption.current];
setSort(sort, event.shiftKey);

View File

@@ -53,32 +53,32 @@ export function GridTable<RecordType extends object>({
[externalFilter],
);
const rowIndexLength = `${data.length}}`.length;
const onKeyDown = useCallback((params: any) => {
const { event, column, data, value, api } = params;
if (
!document.getSelection?.()?.toString?.() &&
event &&
event.key === 'c' &&
(event.ctrlKey || event.metaKey)
) {
const columns =
column.getColId() === PIVOT_COL_ID
? api
.getAllDisplayedColumns()
.filter((column: Column) => column.getColId() !== PIVOT_COL_ID)
: [column];
const record =
column.getColId() === PIVOT_COL_ID
? [
columns.map((column: Column) => column.getColId()).join('\t'),
columns
.map((column: Column) => data?.[column.getColId()])
.join('\t'),
].join('\n')
: String(value);
copyTextToClipboard(() => Promise.resolve(record));
}
}, []);
const onKeyDown: AgGridReactProps<Record<string, any>>['onCellKeyDown'] =
useCallback(({ event, column, data, value, api }) => {
if (
!document.getSelection?.()?.toString?.() &&
event &&
event.key === 'c' &&
(event.ctrlKey || event.metaKey)
) {
const columns =
column.getColId() === PIVOT_COL_ID
? api
.getAllDisplayedColumns()
.filter((column: Column) => column.getColId() !== PIVOT_COL_ID)
: [column];
const record =
column.getColId() === PIVOT_COL_ID
? [
columns.map((column: Column) => column.getColId()).join('\t'),
columns
.map((column: Column) => data?.[column.getColId()])
.join('\t'),
].join('\n')
: String(value);
copyTextToClipboard(() => Promise.resolve(record));
}
}, []);
const columnDefs = useMemo(
() =>
[
@@ -179,7 +179,7 @@ export function GridTable<RecordType extends object>({
isExternalFilterPresent={isExternalFilterPresent}
doesExternalFilterPass={externalFilter}
components={gridComponents}
gridOptions={gridOptions as any}
gridOptions={gridOptions}
onCellKeyDown={onKeyDown}
/>
</div>

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { act } from 'spec/helpers/testing-library';
import {
useModalValidation,

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { renderHook, act } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import {
LocalStorageKeys,
setItem,

View File

@@ -400,7 +400,7 @@ const DashboardBuilder = () => {
}, [dashboardLayout, dispatch]);
const handleDrop = useCallback(
(dropResult: any) => dispatch(handleComponentDrop(dropResult)),
dropResult => dispatch(handleComponentDrop(dropResult)),
[dispatch],
);
@@ -559,7 +559,7 @@ const DashboardBuilder = () => {
: theme.sizeUnit * 8;
const renderChild = useCallback(
(adjustedWidth: any) => {
adjustedWidth => {
const filterBarWidth = dashboardFiltersOpen
? adjustedWidth
: CLOSED_FILTER_BAR_WIDTH;

View File

@@ -279,7 +279,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, [onBeforeUnload]);
const renderTabBar = useCallback(() => <></>, []);
const handleFocus = useCallback((e: any) => {
const handleFocus = useCallback(e => {
if (
// prevent scrolling when tabbing to the tab pane
e.target.classList.contains('ant-tabs-tabpane') &&
@@ -293,7 +293,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, []);
const renderParentSizeChildren = useCallback(
({ width }: { width: any }) => {
({ width }) => {
const tabItems = childIds.map((id, index) => ({
key: index === 0 ? DASHBOARD_GRID_ID : index.toString(),
label: null,

View File

@@ -22,7 +22,7 @@ import { css, styled } from '@apache-superset/core/ui';
import { Constants } from '@superset-ui/core/components';
import { RootState } from 'src/dashboard/types';
import { useSelector } from 'react-redux';
import { useDndMonitor } from '@dnd-kit/core';
import { useDragDropManager } from 'react-dnd';
import classNames from 'classnames';
import { debounce } from 'lodash';
@@ -117,30 +117,34 @@ const DashboardWrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
const editMode = useSelector<RootState, boolean>(
state => state.dashboardState.editMode,
);
const [isDragged, setIsDragged] = useState(false);
const debouncedSetIsDragged = debounce(
setIsDragged,
Constants.FAST_DEBOUNCE,
const dragDropManager = useDragDropManager();
const [isDragged, setIsDragged] = useState(
dragDropManager.getMonitor().isDragging(),
);
useDndMonitor({
onDragStart() {
// set a debounced function to prevent drag source
// from interfering with the drop zone highlighting
debouncedSetIsDragged(true);
},
onDragEnd() {
debouncedSetIsDragged.cancel();
setIsDragged(false);
},
});
useEffect(() => {
const monitor = dragDropManager.getMonitor();
const debouncedSetIsDragged = debounce(
setIsDragged,
Constants.FAST_DEBOUNCE,
);
const unsub = monitor.subscribeToStateChange(() => {
const isDragging = monitor.isDragging();
if (isDragging) {
// set a debounced function to prevent HTML5 drag source
// from interfering with the drop zone highlighting
debouncedSetIsDragged(true);
} else {
debouncedSetIsDragged.cancel();
setIsDragged(false);
}
});
return () => {
unsub();
debouncedSetIsDragged.cancel();
};
}, []);
}, [dragDropManager]);
return (
<StyledDiv

View File

@@ -154,7 +154,7 @@ const PropertiesModal = ({
};
const handleDashboardData = useCallback(
(dashboardData: any) => {
dashboardData => {
const {
id,
dashboard_title,

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