Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Global mode to CategoryWidget & PieWidget #370

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 76 additions & 35 deletions packages/react-widgets/__tests__/models/CategoryModel.test.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,93 @@
import { getCategories } from '../../src/models/CategoryModel';
import { AggregationTypes } from '@carto/react-core';
import { Methods, executeTask } from '@carto/react-workers';
import { executeSQL } from '@carto/react-api/';

const RESULT = [
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
];

jest.mock('@carto/react-api', () => ({
executeSQL: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(RESULT)))
}));

jest.mock('@carto/react-workers', () => ({
executeTask: jest.fn(),
executeTask: jest
.fn()
.mockImplementation(() => new Promise((resolve) => resolve(RESULT))),
Methods: {
FEATURES_CATEGORY: 'featuresCategory'
}
}));

describe('getCategories', () => {
describe('should correctly handle viewport features', () => {
const categoriesParams = {
column: 'storetype',
operationColumn: 'revenue',
operation: AggregationTypes.COUNT,
filters: {},
dataSource: 'whatever-data-source'
};

test('correctly returns data', async () => {
executeTask.mockImplementation(() =>
Promise.resolve([
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
])
describe('local mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'query',
data: 'SELECT * FROM test',
credentials: {
apiVersion: 'v2'
}
},
operation: AggregationTypes.SUM,
column: 'column_1'
};

const data = await getCategories(props);

expect(data).toBe(RESULT);

expect(executeTask).toHaveBeenCalledWith(
props.source.id,
Methods.FEATURES_CATEGORY,
{
filters: props.source.filters,
filtersLogicalOperator: props.source.filtersLogicalOperator,
operation: props.operation,
joinOperation: props.joinOperation,
column: props.column,
operationColumn: props.column
}
);
const categories = await getCategories(categoriesParams);
expect(categories).toEqual([
{ name: 'a', value: 2 },
{ name: 'b', value: 1 }
]);
});
});

describe('global mode', () => {
test('should work correctly', async () => {
const props = {
source: {
id: '__test__',
type: 'table',
data: '__test__',
credentials: {
apiVersion: 'v3',
accessToken: '__test_token__'
},
connection: '__test_connection__'
},
operation: AggregationTypes.SUM,
column: 'column_1',
operationColumn: 'column_2',
global: true
};

const data = await getCategories(props);

expect(data).toBe(RESULT);

test('correctly called', async () => {
const {
column,
operationColumn,
operation,
filters,
dataSource
} = categoriesParams;
await getCategories(categoriesParams);
expect(executeTask).toHaveBeenCalledWith(dataSource, Methods.FEATURES_CATEGORY, {
column,
filters,
operation,
operationColumn
expect(executeSQL).toHaveBeenCalledWith({
credentials: props.source.credentials,
query: `SELECT COALESCE(column_1, 'null') as name, sum(column_2) as value FROM __test__ GROUP BY column_1`,
connection: props.source.connection,
opts: {
abortController: undefined
}
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/react-widgets/src/hooks/useWidgetFetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function useWidgetFetch(
{ id, dataSource, params, global, onError }
) {
// State
const [data, setData] = useState(null);
const [data, setData] = useState();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change here Sergio?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/CartoDB/carto-react/pull/370/files#diff-b6744b2ddd16d9987012b38f43d5800cd351e453fd471af46f6dbb649801e738R59

As you can see in that line, we define a default value. A default value can only be assigned in destructuring when it's undefined, not null.

const [isLoading, setIsLoading] = useState(false);

const isSourceReady = useSelector(
Expand Down
63 changes: 48 additions & 15 deletions packages/react-widgets/src/models/CategoryModel.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,55 @@
import { executeSQL } from '@carto/react-api/';
import { AggregationTypes } from '@carto/react-core/';
import { Methods, executeTask } from '@carto/react-workers';
import {
formatOperationColumn,
formatTableNameWithFilters,
wrapModelCall
} from './utils';

export const getCategories = async (props) => {
const {
column,
operationColumn,
operation,
joinOperation,
filters,
filtersLogicalOperator,
dataSource
} = props;

return executeTask(dataSource, Methods.FEATURES_CATEGORY, {
filters,
filtersLogicalOperator,
export function getCategories(props) {
return wrapModelCall(props, fromLocal, fromRemote);
}

// From local
function fromLocal(props) {
const { source, column, operationColumn, operation, joinOperation } = props;

return executeTask(source.id, Methods.FEATURES_CATEGORY, {
filters: source.filters,
filtersLogicalOperator: source.filtersLogicalOperator,
operation,
joinOperation,
column,
operationColumn: operationColumn || column
});
};
}

// From remote
function fromRemote(props) {
const { source, abortController } = props;
const { credentials, connection } = source;

const query = buildSqlQueryToGetCategories(props);

return executeSQL({
credentials,
query,
connection,
opts: { abortController }
});
}

function buildSqlQueryToGetCategories(props) {
const { column, operation, operationColumn, joinOperation } = props;

const selectValueClause = `${operation}(${
operation === AggregationTypes.COUNT
? '*'
: formatOperationColumn(operationColumn || column, joinOperation)
}) as value`;

return `SELECT COALESCE(${column}, 'null') as name, ${selectValueClause} FROM ${formatTableNameWithFilters(
Copy link
Contributor

@Josmorsot Josmorsot Apr 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, what databases should support this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any of them. Do you know any incompatibility here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop, but it would be nice to involve Data team in these queries verification. Can you gather them (not just this one) and pass them to @Jesus89 please?

props
)} GROUP BY ${column}`.trim();
}
69 changes: 19 additions & 50 deletions packages/react-widgets/src/widgets/CategoryWidget.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { addFilter, removeFilter } from '@carto/react-redux';
import { WrapperWidgetUI, CategoryWidgetUI, NoDataAlert } from '@carto/react-ui';
import { _FilterTypes as FilterTypes, AggregationTypes } from '@carto/react-core';
import { getCategories } from '../models';
import useSourceFilters from '../hooks/useSourceFilters';
import { selectAreFeaturesReadyForSource } from '@carto/react-redux/';
import { useWidgetFilterValues } from '../hooks/useWidgetFilterValues';
import { columnAggregationOn } from './utils/propTypesFns';
import useWidgetFetch from '../hooks/useWidgetFetch';

const EMPTY_ARRAY = [];

Expand All @@ -27,6 +26,7 @@ const EMPTY_ARRAY = [];
* @param {boolean} [props.animation] - Enable/disable widget animations on data updates. Enabled by default.
* @param {boolean} [props.filterable] - Enable/disable widget filtering capabilities. Enabled by default.
* @param {boolean} [props.searchable] - Enable/disable widget searching capabilities. Enabled by default.
* @param {boolean} [props.global] - Enable/disable the viewport filtering in the data fetching.
* @param {Function} [props.onError] - Function to handle error messages from the widget.
* @param {Object} [props.wrapperProps] - Extra props to pass to [WrapperWidgetUI](https://storybook-react.carto.com/?path=/docs/widgets-wrapperwidgetui--default)
* @param {Object} [props.noDataAlertProps] - Extra props to pass to [NoDataAlert]()
Expand All @@ -45,62 +45,29 @@ function CategoryWidget(props) {
animation,
filterable,
searchable,
global,
onError,
wrapperProps,
noDataAlertProps
} = props;
const dispatch = useDispatch();

const isSourceReady = useSelector((state) =>
selectAreFeaturesReadyForSource(state, dataSource)
);

const [categoryData, setCategoryData] = useState([]);

const [isLoading, setIsLoading] = useState(true);

const { filters, filtersLogicalOperator } = useSourceFilters({ dataSource, id });
const selectedCategories =
useWidgetFilterValues({ dataSource, id, column, type: FilterTypes.IN }) ||
EMPTY_ARRAY;

useEffect(() => {
setIsLoading(true);

if (isSourceReady) {
getCategories({
column,
operationColumn,
joinOperation,
operation,
filters,
filtersLogicalOperator,
dataSource
})
.then((data) => {
if (data) {
setIsLoading(false);
setCategoryData(data);
}
})
.catch((error) => {
setIsLoading(false);
if (onError) onError(error);
});
}
}, [
const { data = [], isLoading } = useWidgetFetch(getCategories, {
id,
column,
operationColumn,
joinOperation,
operation,
filters,
filtersLogicalOperator,
dataSource,
setIsLoading,
onError,
isSourceReady
]);
params: {
column,
operationColumn,
joinOperation,
operation
},
global,
onError
});

const handleSelectedCategoriesChange = useCallback(
(categories) => {
Expand Down Expand Up @@ -128,9 +95,9 @@ function CategoryWidget(props) {

return (
<WrapperWidgetUI title={title} isLoading={isLoading} {...wrapperProps}>
{categoryData.length || isLoading ? (
{data.length || isLoading ? (
<CategoryWidgetUI
data={categoryData}
data={data}
formatter={formatter}
labels={labels}
selectedCategories={selectedCategories}
Expand Down Expand Up @@ -162,6 +129,7 @@ CategoryWidget.propTypes = {
animation: PropTypes.bool,
filterable: PropTypes.bool,
searchable: PropTypes.bool,
global: PropTypes.bool,
onError: PropTypes.func,
wrapperProps: PropTypes.object,
noDataAlertProps: PropTypes.object
Expand All @@ -172,6 +140,7 @@ CategoryWidget.defaultProps = {
animation: true,
filterable: true,
searchable: true,
global: false,
wrapperProps: {},
noDataAlertProps: {}
};
Expand Down
Loading