feat: Adds plugin-chart-handlebars (#17903)

* adds: plugin chart handlebars

* adds: handlebars plugin to main presets

* update: npm install

* chore: lint

* adds: dateFormat handlebars helper

* deletes: unused props

* chore: linting plugin-chart-handlebars

* docs: chart-plugin-handlebars

* adds: moment to peer deps

* update: use error handling

* update: inline config, adds renderTrigger

* update: inline config, adds renderTrigger

* camelCase controls

* (plugins-chart-handlebars) adds: missing props

Adds missing propeties in test formData

* (plugin-chart-handlebars) fixes test

* (plugin-handlebars-chart) use numbers for size

* (feature-handlebars-chart) fix viz_type

* (plugin-handlebars-chart) revert

revert the viz_type change. it was in the wrong place.

* fix test and add license headers

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
This commit is contained in:
Jeremy
2022-04-26 06:34:28 -05:00
committed by GitHub
parent 2dafff12ef
commit e632b82395
34 changed files with 1758 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
/**
* 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 {
ColumnOption,
ControlSetItem,
ExtraControlProps,
sharedControls,
} from '@superset-ui/chart-controls';
import {
ensureIsArray,
FeatureFlag,
isFeatureEnabled,
t,
} from '@superset-ui/core';
import React from 'react';
import { getQueryMode, isRawMode } from './shared';
export const allColumns: typeof sharedControls.groupby = {
type: 'SelectControl',
label: t('Columns'),
description: t('Columns to display'),
multi: true,
freeForm: true,
allowAll: true,
commaChoosesOption: false,
default: [],
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: ({ datasource, controls }, controlState) => ({
options: datasource?.columns || [],
queryMode: getQueryMode(controls),
externalValidationErrors:
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
? [t('must have a value')]
: [],
}),
visibility: isRawMode,
};
const dndAllColumns: typeof sharedControls.groupby = {
type: 'DndColumnSelect',
label: t('Columns'),
description: t('Columns to display'),
default: [],
mapStateToProps({ datasource, controls }, controlState) {
const newState: ExtraControlProps = {};
if (datasource) {
const options = datasource.columns;
newState.options = Object.fromEntries(
options.map(option => [option.column_name, option]),
);
}
newState.queryMode = getQueryMode(controls);
newState.externalValidationErrors =
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
? [t('must have a value')]
: [];
return newState;
},
visibility: isRawMode,
};
export const allColumnsControlSetItem: ControlSetItem = {
name: 'all_columns',
config: isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dndAllColumns
: allColumns,
};

View File

@@ -0,0 +1,45 @@
/**
* 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 {
ControlPanelState,
ControlSetItem,
ControlState,
sharedControls,
} from '@superset-ui/chart-controls';
import { isAggMode, validateAggControlValues } from './shared';
export const groupByControlSetItem: ControlSetItem = {
name: 'groupby',
override: {
visibility: isAggMode,
mapStateToProps: (state: ControlPanelState, controlState: ControlState) => {
const { controls } = state;
const originalMapStateToProps = sharedControls?.groupby?.mapStateToProps;
const newState = originalMapStateToProps?.(state, controlState) ?? {};
newState.externalValidationErrors = validateAggControlValues(controls, [
controls.metrics?.value,
controls.percent_metrics?.value,
controlState.value,
]);
return newState;
},
rerender: ['metrics', 'percent_metrics'],
},
};

View File

@@ -0,0 +1,77 @@
/**
* 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 {
ControlSetItem,
CustomControlConfig,
sharedControls,
} from '@superset-ui/chart-controls';
import { t, validateNonEmpty } from '@superset-ui/core';
import React from 'react';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
interface HandlebarsCustomControlProps {
value: string;
}
const HandlebarsTemplateControl = (
props: CustomControlConfig<HandlebarsCustomControlProps>,
) => {
const val = String(
props?.value ? props?.value : props?.default ? props?.default : '',
);
const updateConfig = (source: string) => {
props.onChange(source);
};
return (
<div>
<ControlHeader>{props.label}</ControlHeader>
<CodeEditor
theme="dark"
value={val}
onChange={source => {
updateConfig(source || '');
}}
/>
</div>
);
};
export const handlebarsTemplateControlSetItem: ControlSetItem = {
name: 'handlebarsTemplate',
config: {
...sharedControls.entity,
type: HandlebarsTemplateControl,
label: t('Handlebars Template'),
description: t('A handlebars template that is applied to the data'),
default: `<ul class="data_list">
{{#each data}}
<li>{{this}}</li>
{{/each}}
</ul>`,
isInt: false,
renderTrigger: true,
validators: [validateNonEmpty],
mapStateToProps: ({ controls }) => ({
value: controls?.handlebars_template?.value,
}),
},
};

View File

@@ -0,0 +1,34 @@
/**
* 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 { ControlSetItem } from '@superset-ui/chart-controls';
import { t } from '@superset-ui/core';
import { isAggMode } from './shared';
export const includeTimeControlSetItem: ControlSetItem = {
name: 'include_time',
config: {
type: 'CheckboxControl',
label: t('Include time'),
description: t(
'Whether to include the time granularity as defined in the time section',
),
default: false,
visibility: isAggMode,
},
};

View File

@@ -0,0 +1,38 @@
/**
* 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 {
ControlPanelsContainerProps,
ControlSetItem,
} from '@superset-ui/chart-controls';
import { isAggMode } from './shared';
export const rowLimitControlSetItem: ControlSetItem = {
name: 'row_limit',
override: {
visibility: ({ controls }: ControlPanelsContainerProps) =>
!controls?.server_pagination?.value,
},
};
export const timeSeriesLimitMetricControlSetItem: ControlSetItem = {
name: 'timeseries_limit_metric',
override: {
visibility: isAggMode,
},
};

View File

@@ -0,0 +1,103 @@
/**
* 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 {
ControlPanelState,
ControlSetItem,
ControlState,
sharedControls,
} from '@superset-ui/chart-controls';
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import { getQueryMode, isAggMode, validateAggControlValues } from './shared';
const percentMetrics: typeof sharedControls.metrics = {
type: 'MetricsControl',
label: t('Percentage metrics'),
description: t(
'Metrics for which percentage of total are to be displayed. Calculated from only data within the row limit.',
),
multi: true,
visibility: isAggMode,
mapStateToProps: ({ datasource, controls }, controlState) => ({
columns: datasource?.columns || [],
savedMetrics: datasource?.metrics || [],
datasource,
datasourceType: datasource?.type,
queryMode: getQueryMode(controls),
externalValidationErrors: validateAggControlValues(controls, [
controls.groupby?.value,
controls.metrics?.value,
controlState.value,
]),
}),
rerender: ['groupby', 'metrics'],
default: [],
validators: [],
};
const dndPercentMetrics = {
...percentMetrics,
type: 'DndMetricSelect',
};
export const percentMetricsControlSetItem: ControlSetItem = {
name: 'percent_metrics',
config: {
...(isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dndPercentMetrics
: percentMetrics),
},
};
export const metricsControlSetItem: ControlSetItem = {
name: 'metrics',
override: {
validators: [],
visibility: isAggMode,
mapStateToProps: (
{ controls, datasource, form_data }: ControlPanelState,
controlState: ControlState,
) => ({
columns: datasource?.columns.filter(c => c.filterable) || [],
savedMetrics: datasource?.metrics || [],
// current active adhoc metrics
selectedMetrics:
form_data.metrics || (form_data.metric ? [form_data.metric] : []),
datasource,
externalValidationErrors: validateAggControlValues(controls, [
controls.groupby?.value,
controls.percent_metrics?.value,
controlState.value,
]),
}),
rerender: ['groupby', 'percent_metrics'],
},
};
export const showTotalsControlSetItem: ControlSetItem = {
name: 'show_totals',
config: {
type: 'CheckboxControl',
label: t('Show totals'),
default: false,
description: t(
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
),
visibility: isAggMode,
},
};

View File

@@ -0,0 +1,47 @@
/**
* 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 { ControlSetItem } from '@superset-ui/chart-controls';
import { t } from '@superset-ui/core';
import { isAggMode, isRawMode } from './shared';
export const orderByControlSetItem: ControlSetItem = {
name: 'order_by_cols',
config: {
type: 'SelectControl',
label: t('Ordering'),
description: t('Order results by selected columns'),
multi: true,
default: [],
mapStateToProps: ({ datasource }) => ({
choices: datasource?.order_by_choices || [],
}),
visibility: isRawMode,
},
};
export const orderDescendingControlSetItem: ControlSetItem = {
name: 'order_desc',
config: {
type: 'CheckboxControl',
label: t('Sort descending'),
default: true,
description: t('Whether to sort descending or ascending'),
visibility: isAggMode,
},
};

View File

@@ -0,0 +1,57 @@
/**
* 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 {
ControlPanelsContainerProps,
ControlSetItem,
ControlSetRow,
} from '@superset-ui/chart-controls';
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import { PAGE_SIZE_OPTIONS } from '../../consts';
export const serverPaginationControlSetRow: ControlSetRow =
isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS) ||
isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)
? [
{
name: 'server_pagination',
config: {
type: 'CheckboxControl',
label: t('Server pagination'),
description: t(
'Enable server side pagination of results (experimental feature)',
),
default: false,
},
},
]
: [];
export const serverPageLengthControlSetItem: ControlSetItem = {
name: 'server_page_length',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Server Page Length'),
default: 10,
choices: PAGE_SIZE_OPTIONS,
description: t('Rows per page, 0 means no pagination'),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.server_pagination?.value),
},
};

View File

@@ -0,0 +1,42 @@
/**
* 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 {
ControlConfig,
ControlSetItem,
QueryModeLabel,
} from '@superset-ui/chart-controls';
import { QueryMode, t } from '@superset-ui/core';
import { getQueryMode } from './shared';
const queryMode: ControlConfig<'RadioButtonControl'> = {
type: 'RadioButtonControl',
label: t('Query mode'),
default: null,
options: [
[QueryMode.aggregate, QueryModeLabel[QueryMode.aggregate]],
[QueryMode.raw, QueryModeLabel[QueryMode.raw]],
],
mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }),
rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'],
};
export const queryModeControlSetItem: ControlSetItem = {
name: 'query_mode',
config: queryMode,
};

View File

@@ -0,0 +1,61 @@
/**
* 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 {
ControlPanelsContainerProps,
ControlStateMapping,
} from '@superset-ui/chart-controls';
import {
ensureIsArray,
QueryFormColumn,
QueryMode,
t,
} from '@superset-ui/core';
export function getQueryMode(controls: ControlStateMapping): QueryMode {
const mode = controls?.query_mode?.value;
if (mode === QueryMode.aggregate || mode === QueryMode.raw) {
return mode as QueryMode;
}
const rawColumns = controls?.all_columns?.value as
| QueryFormColumn[]
| undefined;
const hasRawColumns = rawColumns && rawColumns.length > 0;
return hasRawColumns ? QueryMode.raw : QueryMode.aggregate;
}
/**
* Visibility check
*/
export function isQueryMode(mode: QueryMode) {
return ({ controls }: Pick<ControlPanelsContainerProps, 'controls'>) =>
getQueryMode(controls) === mode;
}
export const isAggMode = isQueryMode(QueryMode.aggregate);
export const isRawMode = isQueryMode(QueryMode.raw);
export const validateAggControlValues = (
controls: ControlStateMapping,
values: any[],
) => {
const areControlsEmpty = values.every(val => ensureIsArray(val).length === 0);
return areControlsEmpty && isAggMode({ controls })
? [t('Group By, Metrics or Percentage Metrics must have a value')]
: [];
};

View File

@@ -0,0 +1,72 @@
/**
* 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 {
ControlSetItem,
CustomControlConfig,
sharedControls,
} from '@superset-ui/chart-controls';
import { t } from '@superset-ui/core';
import React from 'react';
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
interface StyleCustomControlProps {
value: string;
}
const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
const val = String(
props?.value ? props?.value : props?.default ? props?.default : '',
);
const updateConfig = (source: string) => {
props.onChange(source);
};
return (
<div>
<ControlHeader>{props.label}</ControlHeader>
<CodeEditor
theme="dark"
mode="css"
value={val}
onChange={source => {
updateConfig(source || '');
}}
/>
</div>
);
};
export const styleControlSetItem: ControlSetItem = {
name: 'styleTemplate',
config: {
...sharedControls.entity,
type: StyleControl,
label: t('CSS Styles'),
description: t('CSS applied to the chart'),
default: '',
isInt: false,
renderTrigger: true,
validators: [],
mapStateToProps: ({ controls }) => ({
value: controls?.handlebars_template?.value,
}),
},
};