Skip to content

Commit

Permalink
Revert "chore(fe): remove unused codes"
Browse files Browse the repository at this point in the history
This reverts commit 996a671.
  • Loading branch information
hainenber committed Sep 17, 2024
1 parent e3b1474 commit 99560dd
Show file tree
Hide file tree
Showing 2 changed files with 362 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* 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 { ReactWrapper } from 'enzyme';
import { styledMount as mount } from 'spec/helpers/theming';
import { act } from 'react-dom/test-utils';

import withAsyncVerification, {
ControlPropsWithExtras,
WithAsyncVerificationOptions,
} from 'src/explore/components/controls/withAsyncVerification';
import { ExtraControlProps } from '@superset-ui/chart-controls';
import MetricsControl from 'src/explore/components/controls/MetricControl/MetricsControl';

const VALID_METRIC = {
metric_name: 'sum__value',
expression: 'SUM(energy_usage.value)',
};

const mockSetControlValue = jest.fn();

const defaultProps = {
name: 'metrics',
label: 'Metrics',
value: undefined,
multi: true,
needAsyncVerification: true,
actions: { setControlValue: mockSetControlValue },
onChange: () => {},
columns: [
{ type: 'VARCHAR(255)', column_name: 'source' },
{ type: 'VARCHAR(255)', column_name: 'target' },
{ type: 'DOUBLE', column_name: 'value' },
],
savedMetrics: [
VALID_METRIC,
{ metric_name: 'avg__value', expression: 'AVG(energy_usage.value)' },
],
datasourceType: 'sqla',
};

function verify(sourceProp: string) {
const mock = jest.fn();
mock.mockImplementation(async (props: ControlPropsWithExtras) => ({
[sourceProp]: props.validMetrics || [VALID_METRIC],
}));
return mock;
}

async function setup({
extraProps,
baseControl = MetricsControl as WithAsyncVerificationOptions['baseControl'],
onChange,
}: Partial<WithAsyncVerificationOptions> & {
extraProps?: ExtraControlProps;
} = {}) {
const props = {
...defaultProps,
...extraProps,
};
const verifier = verify('savedMetrics');
const VerifiedControl = withAsyncVerification({
baseControl,
verify: verifier,
onChange,
});
type Wrapper = ReactWrapper<typeof props & ExtraControlProps>;
let wrapper: Wrapper | undefined;
await act(async () => {
wrapper = mount(<VerifiedControl {...props} />);
});
return { props, wrapper: wrapper as Wrapper, onChange, verifier };
}

describe('VerifiedMetricsControl', () => {
it('should calls verify correctly', async () => {
expect.assertions(5);
const { wrapper, verifier, props } = await setup();

expect(wrapper.find(MetricsControl).length).toBe(1);

expect(verifier).toBeCalledTimes(1);
expect(verifier).toBeCalledWith(
expect.objectContaining({ savedMetrics: props.savedMetrics }),
);

// should call verifier with new props when props are updated.
await act(async () => {
wrapper.setProps({ validMetric: ['abc'] });
});

expect(verifier).toBeCalledTimes(2);
expect(verifier).toBeCalledWith(
expect.objectContaining({ validMetric: ['abc'] }),
);
});

it('should trigger onChange event', async () => {
expect.assertions(3);
const mockOnChange = jest.fn();
const { wrapper } = await setup({
// should allow specify baseControl with control component name
baseControl: 'MetricsControl',
onChange: mockOnChange,
});

const child = wrapper.find(MetricsControl);
child.props().onChange?.(['abc']);

expect(child.length).toBe(1);
expect(mockOnChange).toBeCalledTimes(1);
expect(mockOnChange).toBeCalledWith(['abc'], {
actions: defaultProps.actions,
columns: defaultProps.columns,
datasourceType: defaultProps.datasourceType,
label: defaultProps.label,
multi: defaultProps.multi,
name: defaultProps.name,
// in real life, `onChange` should have been called with the updated
// props (both savedMetrics and value should have been updated), but
// because of the limitation of enzyme (it cannot get props updated from
// useEffect hooks), we are not able to check that here.
savedMetrics: defaultProps.savedMetrics,
value: undefined,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* 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 { ComponentType, useCallback, useEffect, useRef, useState } from 'react';
import {
ExtraControlProps,
sharedControlComponents,
} from '@superset-ui/chart-controls';
import { JsonArray, JsonValue, t } from '@superset-ui/core';
import { ControlProps } from 'src/explore/components/Control';
import builtInControlComponents from 'src/explore/components/controls';

/**
* Full control component map.
*/
const controlComponentMap = {
...builtInControlComponents,
...sharedControlComponents,
};

export type SharedControlComponent = keyof typeof controlComponentMap;

/**
* The actual props passed to the control component itself
* (not src/explore/components/Control.tsx).
*/
export type ControlPropsWithExtras = Omit<ControlProps, 'type'> &
ExtraControlProps;

/**
* The full props passed to control component. Including withAsyncVerification
* related props and `onChange` event + `hovered` state from Control.tsx.
*/
export type FullControlProps = ControlPropsWithExtras & {
onChange?: (value: JsonValue) => void;
hovered?: boolean;
/**
* An extra flag for triggering async verification. Set it in mapStateToProps.
*/
needAsyncVerification?: boolean;
/**
* Whether to show loading state when verification is still loading.
*/
showLoadingState?: boolean;
verify?: AsyncVerify;
};

/**
* The async verification function that accepts control props and returns a
* promise resolving to extra props if overrides are needed.
*/
export type AsyncVerify = (
props: ControlPropsWithExtras,
) => Promise<ExtraControlProps | undefined | null>;

/**
* Whether the extra props will update the original props.
*/
function hasUpdates(
props: ControlPropsWithExtras,
newProps: ExtraControlProps,
) {
return (
props !== newProps &&
Object.entries(newProps).some(([key, value]) => {
if (Array.isArray(props[key]) && Array.isArray(value)) {
const sourceValue: JsonArray = props[key];
return (
sourceValue.length !== value.length ||
sourceValue.some((x, i) => x !== value[i])
);
}
if (key === 'formData') {
return JSON.stringify(props[key]) !== JSON.stringify(value);
}
return props[key] !== value;
})
);
}

export type WithAsyncVerificationOptions = {
baseControl:
| SharedControlComponent
// allows custom `baseControl` to not handle some of the <Control />
// component props.
| ComponentType<Partial<FullControlProps>>;
showLoadingState?: boolean;
quiet?: boolean;
verify?: AsyncVerify;
onChange?: (value: JsonValue, props: ControlPropsWithExtras) => void;
};

/**
* Wrap Control with additional async verification. The <Control /> component
* will render twice, once with the original props, then later with the updated
* props after the async verification is finished.
*
* @param baseControl - The base control component.
* @param verify - An async function that returns a Promise which resolves with
* the updated and verified props. You should handle error within
* the promise itself. If the Promise returns nothing or null, then
* the control will not rerender.
* @param onChange - Additional event handler when values are changed by users.
* @param quiet - Whether to show a warning toast when verification failed.
*/
export default function withAsyncVerification({
baseControl,
onChange,
verify: defaultVerify,
quiet = false,
showLoadingState: defaultShowLoadingState = true,
}: WithAsyncVerificationOptions) {
const ControlComponent: ComponentType<FullControlProps> =
typeof baseControl === 'string'
? controlComponentMap[baseControl]
: baseControl;

return function ControlWithVerification(props: FullControlProps) {
const {
hovered,
onChange: basicOnChange,
needAsyncVerification = false,
isLoading: initialIsLoading = false,
showLoadingState = defaultShowLoadingState,
verify = defaultVerify,
...restProps
} = props;
const otherPropsRef = useRef(restProps);
const [verifiedProps, setVerifiedProps] = useState({});
const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading);
const { addWarningToast } = restProps.actions;

// memoize `restProps`, so that verification only triggers when material
// props are actually updated.
let otherProps = otherPropsRef.current;
if (hasUpdates(otherProps, restProps)) {
otherProps = otherPropsRef.current = restProps;
}

const handleChange = useCallback(
(value: JsonValue) => {
// the default onChange handler, triggers the `setControlValue` action
if (basicOnChange) {
basicOnChange(value);
}
if (onChange) {
onChange(value, { ...otherProps, ...verifiedProps });
}
},
[basicOnChange, otherProps, verifiedProps],
);

useEffect(() => {
if (needAsyncVerification && verify) {
if (showLoadingState) {
setIsLoading(true);
}
verify(otherProps)
.then(updatedProps => {
if (showLoadingState) {
setIsLoading(false);
}
if (updatedProps && hasUpdates(otherProps, updatedProps)) {
setVerifiedProps({
// save isLoading in combination with other props to avoid
// rendering twice.
...updatedProps,
});
}
})
.catch((err: Error | string) => {
if (showLoadingState) {
setIsLoading(false);
}
if (!quiet && addWarningToast) {
addWarningToast(
t(
'Failed to verify select options: %s',
(typeof err === 'string' ? err : err.message) ||
t('[unknown error]'),
),
{ noDuplicate: true },
);
}
});
}
}, [
needAsyncVerification,
showLoadingState,
verify,
otherProps,
addWarningToast,
]);

return (
<ControlComponent
isLoading={isLoading}
hovered={hovered}
onChange={handleChange}
{...otherProps}
{...verifiedProps}
/>
);
};
}

0 comments on commit 99560dd

Please sign in to comment.