mirror of
https://github.com/apache/superset.git
synced 2026-06-13 19:49:18 +00:00
Compare commits
27 Commits
msyavuz/ch
...
semantic-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ade0915d0 | ||
|
|
3596ef304e | ||
|
|
acb8b63023 | ||
|
|
b9ab0ced77 | ||
|
|
bfbb68c3c8 | ||
|
|
b437421a8e | ||
|
|
e253bd2fb3 | ||
|
|
bfb7048e42 | ||
|
|
2833b69ca0 | ||
|
|
6e17714a19 | ||
|
|
8a0aaa42ec | ||
|
|
af479a9d99 | ||
|
|
77f60f42e6 | ||
|
|
f0121a166e | ||
|
|
0c4b0cb9b9 | ||
|
|
a36bbf8ffd | ||
|
|
99525c1ce9 | ||
|
|
889e9bbade | ||
|
|
b809a990ee | ||
|
|
9c7fcbf548 | ||
|
|
046aabee73 | ||
|
|
b672c7b853 | ||
|
|
ea33d797a7 | ||
|
|
ab8352ee66 | ||
|
|
bf2cef7d87 | ||
|
|
a6b6eb4ab3 | ||
|
|
cac6ffcd3c |
@@ -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]
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
5
extensions/requirements-snowflake.txt
Normal file
5
extensions/requirements-snowflake.txt
Normal 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
|
||||
BIN
extensions/superset-snowflake-semantic-layer-1.0.0.supx
Normal file
BIN
extensions/superset-snowflake-semantic-layer-1.0.0.supx
Normal file
Binary file not shown.
@@ -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',
|
||||
{
|
||||
|
||||
27631
superset-frontend/package-lock.json
generated
27631
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "*"
|
||||
},
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 '.';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -156,7 +156,7 @@ export interface QueryObject
|
||||
|
||||
export interface QueryContext {
|
||||
datasource: {
|
||||
id: number;
|
||||
id: number | string;
|
||||
type: DatasourceType;
|
||||
};
|
||||
/** Force refresh of all queries */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"mapbox-gl": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -196,5 +196,5 @@ export const DeckGLContainerStyledWrapper = styled(DeckGLContainer)`
|
||||
`;
|
||||
|
||||
export type DeckGLContainerHandle = typeof DeckGLContainer & {
|
||||
setTooltip: (tooltip: TooltipProps['tooltip']) => void;
|
||||
setTooltip: (tooltip: ReactNode) => void;
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
@@ -67,5 +67,5 @@ export const TextCellRenderer = (params: CellRendererProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
return <div>{String(valueFormatted ?? value)}</div>;
|
||||
return <div>{valueFormatted ?? value}</div>;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"echarts": "*",
|
||||
"memoize-one": "*",
|
||||
"react": "^18.2.0"
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -70,7 +70,7 @@ ${helperDescriptions
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{props.label as any}
|
||||
{props.label}
|
||||
<InfoTooltip
|
||||
iconStyle={{ marginLeft: theme.sizeUnit }}
|
||||
tooltip={<SafeMarkdown source={helpersTooltipContent} />}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ const RunQueryActionButton = ({
|
||||
}
|
||||
: {
|
||||
buttonStyle: shouldShowStopBtn ? 'danger' : 'primary',
|
||||
icon: icon as any,
|
||||
icon,
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -580,7 +580,7 @@ const SqlEditor: FC<Props> = ({
|
||||
};
|
||||
|
||||
const setQueryEditorAndSaveSql = useCallback(
|
||||
(sql: any) => {
|
||||
sql => {
|
||||
dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
|
||||
},
|
||||
[dispatch, queryEditor],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 '.';
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}`,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -116,7 +116,7 @@ export const useDrillDetailMenuItems = ({
|
||||
);
|
||||
|
||||
const openModal = useCallback(
|
||||
(filters: any, event: any) => {
|
||||
(filters, event) => {
|
||||
onClick(event);
|
||||
onSelection();
|
||||
setFilters(filters);
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Field<V>({
|
||||
errorMessage,
|
||||
}: FieldProps<V>) {
|
||||
const onControlChange = useCallback(
|
||||
(newValue: any) => {
|
||||
newValue => {
|
||||
onChange(fieldKey, newValue);
|
||||
},
|
||||
[onChange, fieldKey],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user