mirror of
https://github.com/apache/superset.git
synced 2026-05-28 19:25:20 +00:00
feat(glyph): single-file chart definition pattern across all plugins
Introduce `defineChart()` — a declarative pattern that bundles metadata,
arguments (control-panel config), buildQuery, transform, and render into
a single chart-plugin file. Migrate every chart plugin to this pattern:
* plugin-chart-echarts: Pie, Funnel, Gauge, Sankey, Waterfall,
Histogram, Tree, Bubble, BoxPlot, Sunburst, Radar, Treemap, Graph,
Heatmap, Gantt, BigNumber (Total, WithTrendline, PoP, Glyph demo),
MixedTimeseries, and the Timeseries family (Generic, Scatter,
SmoothLine, Step, Area, Line, Bar)
* legacy-plugin-chart-*: calendar, horizon, chord, country-map,
world-map, paired-t-test, parallel-coordinates, partition, rose,
map-box
* other plugins: handlebars, word-cloud, pivot-table, table,
ag-grid-table, cartodiagram
* legacy-preset-chart-nvd3: Bubble, Bullet, Compare, TimePivot
* legacy-preset-chart-deckgl: Grid, Hex, Polygon, Scatter (single-file
defineChart); Arc, Contour, Geojson, Heatmap, Path, Screengrid kept
on the original multi-file ChartPlugin pattern pending follow-up
Glyph-core lives as @superset-ui/glyph-core (extracted package) and
provides: defineChart, ~14 argument types (Metric, Dimension, Select,
Checkbox, Text, Int, Slider, etc.), reusable presets (ShowLegend,
HeaderFontSize, Subtitle, etc.), cross-filter utilities
(extractCrossFilterProps, createSelectedValuesMap, isDataPointFiltered,
createLabelMap), and visibility-condition helpers
(resolveArgClass, getArgVisibleWhen, evaluateGlyphCondition).
Customize-tab rendering uses a new GlyphOptionsPanel — a native React
renderer that hybrids glyph args with additionalControls, with
inlined sharedControls in the Query section.
Imports are routed through @apache-superset/core subpath entrypoints
(/translation for t, /common for GenericDataType).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +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 { styled } from '@apache-superset/core/theme';
|
||||
import { createRef } from 'react';
|
||||
import { HandlebarsViewer } from './components/Handlebars/HandlebarsViewer';
|
||||
import { HandlebarsProps, HandlebarsStylesProps } from './types';
|
||||
|
||||
const Styles = styled.div<HandlebarsStylesProps>`
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
height: ${({ height }) => height}px;
|
||||
width: ${({ width }) => width}px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export default function Handlebars(props: HandlebarsProps) {
|
||||
const { data, height, width, formData } = props;
|
||||
const styleTemplateSource = formData.styleTemplate
|
||||
? `<style>${formData.styleTemplate}</style>`
|
||||
: '';
|
||||
const handlebarTemplateSource = formData.handlebarsTemplate
|
||||
? formData.handlebarsTemplate
|
||||
: '{{data}}';
|
||||
const templateSource = `${handlebarTemplateSource}\n${styleTemplateSource} `;
|
||||
|
||||
const rootElem = createRef<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<Styles ref={rootElem} height={height} width={width}>
|
||||
<HandlebarsViewer data={{ data }} templateSource={templateSource} />
|
||||
</Styles>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +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.
|
||||
*/
|
||||
|
||||
export {
|
||||
CodeEditor,
|
||||
type CodeEditorProps,
|
||||
type CodeEditorMode,
|
||||
type CodeEditorTheme,
|
||||
} from '@superset-ui/core/components';
|
||||
@@ -1,31 +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 { ReactNode } from 'react';
|
||||
|
||||
interface ControlHeaderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ControlHeader = ({
|
||||
children,
|
||||
}: ControlHeaderProps): JSX.Element => (
|
||||
<div className="ControlHeader">
|
||||
<div className="pull-left">{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,118 +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 { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { SafeMarkdown } from '@superset-ui/core/components';
|
||||
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
|
||||
import Handlebars from 'handlebars';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import Helpers from 'just-handlebars-helpers';
|
||||
import HandlebarsGroupBy from 'handlebars-group-by';
|
||||
|
||||
export interface HandlebarsViewerProps {
|
||||
templateSource: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const HandlebarsViewer = ({
|
||||
templateSource,
|
||||
data,
|
||||
}: HandlebarsViewerProps) => {
|
||||
const [renderedTemplate, setRenderedTemplate] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const appContainer = document.getElementById('app');
|
||||
const { common } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
const htmlSanitization = common?.conf?.HTML_SANITIZATION ?? true;
|
||||
const htmlSchemaOverrides =
|
||||
common?.conf?.HTML_SANITIZATION_SCHEMA_EXTENSIONS || {};
|
||||
|
||||
useMemo(() => {
|
||||
try {
|
||||
const template = Handlebars.compile(templateSource);
|
||||
const result = template(data);
|
||||
setRenderedTemplate(result);
|
||||
setError('');
|
||||
} catch (error) {
|
||||
setRenderedTemplate('');
|
||||
setError(error.message);
|
||||
}
|
||||
}, [templateSource, data]);
|
||||
|
||||
const Error = styled.pre`
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
if (error) {
|
||||
return <Error>{error}</Error>;
|
||||
}
|
||||
|
||||
if (renderedTemplate) {
|
||||
return (
|
||||
<SafeMarkdown
|
||||
source={renderedTemplate}
|
||||
htmlSanitization={htmlSanitization}
|
||||
htmlSchemaOverrides={htmlSchemaOverrides}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>{t('Loading...')}</p>;
|
||||
};
|
||||
|
||||
// usage: {{ dateFormat my_date format="MMMM YYYY" }}
|
||||
Handlebars.registerHelper('dateFormat', function (context, block) {
|
||||
const f = block.hash.format || 'YYYY-MM-DD';
|
||||
return dayjs(context).format(f);
|
||||
});
|
||||
|
||||
// usage: {{ }}
|
||||
Handlebars.registerHelper('stringify', (obj: any, obj2: any) => {
|
||||
// calling without an argument
|
||||
if (obj2 === undefined)
|
||||
throw new Error('Please call with an object. Example: `stringify myObj`');
|
||||
return isPlainObject(obj) ? JSON.stringify(obj) : String(obj);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper(
|
||||
'formatNumber',
|
||||
function (number: any, locale = 'en-US') {
|
||||
if (typeof number !== 'number') {
|
||||
return number;
|
||||
}
|
||||
return number.toLocaleString(locale);
|
||||
},
|
||||
);
|
||||
|
||||
// usage: {{parseJson jsonString}}
|
||||
Handlebars.registerHelper('parseJson', (jsonString: string) => {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
error.message = `Invalid JSON string: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Invalid JSON string: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
Helpers.registerHelpers(Handlebars);
|
||||
HandlebarsGroupBy.register(Handlebars);
|
||||
@@ -1,25 +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 { debounce } from 'lodash';
|
||||
import { Constants } from '@superset-ui/core/components';
|
||||
|
||||
export const debounceFunc = debounce(
|
||||
(func: (val: string) => void, source: string) => func(source),
|
||||
Constants.SLOW_DEBOUNCE,
|
||||
);
|
||||
@@ -1,27 +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.
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as HandlebarsChartPlugin } from './plugin';
|
||||
/**
|
||||
* Note: this file exports the default export from Handlebars.tsx.
|
||||
* If you want to export multiple visualization modules, you will need to
|
||||
* either add additional plugin folders (similar in structure to ./plugin)
|
||||
* OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts
|
||||
* which in turn load exports from Handlebars.tsx
|
||||
*/
|
||||
676
superset-frontend/plugins/plugin-chart-handlebars/src/index.tsx
Normal file
676
superset-frontend/plugins/plugin-chart-handlebars/src/index.tsx
Normal file
@@ -0,0 +1,676 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handlebars Chart - Glyph Pattern Implementation
|
||||
*
|
||||
* Renders data via a user-supplied Handlebars template with optional CSS.
|
||||
* Supports both Aggregate and Raw query modes.
|
||||
*/
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { styled, useTheme } from '@apache-superset/core/theme';
|
||||
import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
normalizeOrderBy,
|
||||
QueryFormColumn,
|
||||
QueryFormData,
|
||||
QueryFormMetric,
|
||||
QueryMode,
|
||||
TimeGranularity,
|
||||
TimeseriesDataRecord,
|
||||
validateNonEmpty,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
CodeEditor,
|
||||
Constants,
|
||||
InfoTooltip,
|
||||
SafeMarkdown,
|
||||
} from '@superset-ui/core/components';
|
||||
import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
|
||||
import {
|
||||
ColumnMeta,
|
||||
ControlConfig,
|
||||
ControlPanelState,
|
||||
ControlPanelsContainerProps,
|
||||
ControlSetItem,
|
||||
ControlState,
|
||||
ControlStateMapping,
|
||||
CustomControlConfig,
|
||||
Dataset,
|
||||
defineSavedMetrics,
|
||||
ExtraControlProps,
|
||||
getStandardizedControls,
|
||||
QueryModeLabel,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import Handlebars from 'handlebars';
|
||||
import { debounce, isEmpty, isPlainObject } from 'lodash';
|
||||
import Helpers from 'just-handlebars-helpers';
|
||||
import HandlebarsGroupBy from 'handlebars-group-by';
|
||||
import { createRef, ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/example1.jpg';
|
||||
import example1Dark from './images/example1-dark.jpg';
|
||||
import example2 from './images/example2.jpg';
|
||||
import example2Dark from './images/example2-dark.jpg';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export type HandlebarsQueryFormData = QueryFormData & {
|
||||
height: number;
|
||||
width: number;
|
||||
handlebarsTemplate?: string;
|
||||
styleTemplate?: string;
|
||||
align_pn?: boolean;
|
||||
color_pn?: boolean;
|
||||
include_time?: boolean;
|
||||
include_search?: boolean;
|
||||
query_mode?: QueryMode;
|
||||
page_length?: string | number | null;
|
||||
metrics?: QueryFormMetric[] | null;
|
||||
percent_metrics?: QueryFormMetric[] | null;
|
||||
timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null;
|
||||
groupby?: QueryFormMetric[] | null;
|
||||
all_columns?: QueryFormMetric[] | null;
|
||||
order_desc?: boolean;
|
||||
table_timestamp_format?: string;
|
||||
granularitySqla?: string;
|
||||
time_grain_sqla?: TimeGranularity;
|
||||
};
|
||||
|
||||
type HandlebarsProps = {
|
||||
height: number;
|
||||
width: number;
|
||||
data: TimeseriesDataRecord[];
|
||||
formData: HandlebarsQueryFormData;
|
||||
};
|
||||
|
||||
// ─── Helpers & Utilities ─────────────────────────────────────────────────────
|
||||
|
||||
const debounceFunc = debounce(
|
||||
(func: (val: string) => void, source: string) => func(source),
|
||||
Constants.SLOW_DEBOUNCE,
|
||||
);
|
||||
|
||||
const ControlHeader = ({ children }: { children: ReactNode }) => (
|
||||
<div className="ControlHeader">
|
||||
<div className="pull-left">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Handlebars Template Engine Setup ────────────────────────────────────────
|
||||
|
||||
// usage: {{ dateFormat my_date format="MMMM YYYY" }}
|
||||
Handlebars.registerHelper('dateFormat', function (context, block) {
|
||||
const f = block.hash.format || 'YYYY-MM-DD';
|
||||
return dayjs(context).format(f);
|
||||
});
|
||||
|
||||
// usage: {{ stringify myObj }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Handlebars.registerHelper('stringify', (obj: any, obj2: any) => {
|
||||
if (obj2 === undefined)
|
||||
throw new Error('Please call with an object. Example: `stringify myObj`');
|
||||
return isPlainObject(obj) ? JSON.stringify(obj) : String(obj);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Handlebars.registerHelper(
|
||||
'formatNumber',
|
||||
function (number: any, locale = 'en-US') {
|
||||
if (typeof number !== 'number') return number;
|
||||
return number.toLocaleString(locale);
|
||||
},
|
||||
);
|
||||
|
||||
// usage: {{parseJson jsonString}}
|
||||
Handlebars.registerHelper('parseJson', (jsonString: string) => {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
error.message = `Invalid JSON string: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Invalid JSON string: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
Helpers.registerHelpers(Handlebars);
|
||||
HandlebarsGroupBy.register(Handlebars);
|
||||
|
||||
// ─── HandlebarsViewer ────────────────────────────────────────────────────────
|
||||
|
||||
const TemplateError = styled.pre`
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
function HandlebarsViewer({
|
||||
templateSource,
|
||||
data,
|
||||
}: {
|
||||
templateSource: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any;
|
||||
}) {
|
||||
const [renderedTemplate, setRenderedTemplate] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const appContainer = document.getElementById('app');
|
||||
const { common } = JSON.parse(
|
||||
appContainer?.getAttribute('data-bootstrap') || '{}',
|
||||
);
|
||||
const htmlSanitization = common?.conf?.HTML_SANITIZATION ?? true;
|
||||
const htmlSchemaOverrides =
|
||||
common?.conf?.HTML_SANITIZATION_SCHEMA_EXTENSIONS || {};
|
||||
|
||||
useMemo(() => {
|
||||
try {
|
||||
const template = Handlebars.compile(templateSource);
|
||||
const result = template(data);
|
||||
setRenderedTemplate(result);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setRenderedTemplate('');
|
||||
setError((err as Error).message);
|
||||
}
|
||||
}, [templateSource, data]);
|
||||
|
||||
if (error) return <TemplateError>{error}</TemplateError>;
|
||||
if (renderedTemplate) {
|
||||
return (
|
||||
<SafeMarkdown
|
||||
source={renderedTemplate}
|
||||
htmlSanitization={htmlSanitization}
|
||||
htmlSchemaOverrides={htmlSchemaOverrides}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <p>{t('Loading...')}</p>;
|
||||
}
|
||||
|
||||
// ─── Render Component ────────────────────────────────────────────────────────
|
||||
|
||||
const HandlebarsStyles = styled.div<{ height: number; width: number }>`
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
height: ${({ height }) => height}px;
|
||||
width: ${({ width }) => width}px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
function HandlebarsChart(props: HandlebarsProps) {
|
||||
const { data, height, width, formData } = props;
|
||||
const styleTemplateSource = formData.styleTemplate
|
||||
? `<style>${formData.styleTemplate}</style>`
|
||||
: '';
|
||||
const templateSource = `${formData.handlebarsTemplate || '{{data}}'}\n${styleTemplateSource} `;
|
||||
const rootElem = createRef<HTMLDivElement>();
|
||||
return (
|
||||
<HandlebarsStyles ref={rootElem} height={height} width={width}>
|
||||
<HandlebarsViewer data={{ data }} templateSource={templateSource} />
|
||||
</HandlebarsStyles>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Query Mode Utilities ─────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
return rawColumns?.length ? QueryMode.Raw : QueryMode.Aggregate;
|
||||
}
|
||||
|
||||
function isQueryMode(mode: QueryMode) {
|
||||
return ({ controls }: Pick<ControlPanelsContainerProps, 'controls'>) =>
|
||||
getQueryMode(controls) === mode;
|
||||
}
|
||||
|
||||
const isAggMode = isQueryMode(QueryMode.Aggregate);
|
||||
const isRawMode = isQueryMode(QueryMode.Raw);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function 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')]
|
||||
: [];
|
||||
}
|
||||
|
||||
// ─── Controls ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const queryModeControlSetItem: ControlSetItem = {
|
||||
name: 'query_mode',
|
||||
config: {
|
||||
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'],
|
||||
} as ControlConfig<'RadioButtonControl'>,
|
||||
};
|
||||
|
||||
const groupByControlSetItem: ControlSetItem = {
|
||||
name: 'groupby',
|
||||
override: {
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
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'],
|
||||
},
|
||||
};
|
||||
|
||||
const allColumnsControlSetItem: ControlSetItem = {
|
||||
name: 'all_columns',
|
||||
config: {
|
||||
type: 'DndColumnSelect',
|
||||
label: t('Columns'),
|
||||
description: t('Columns to display'),
|
||||
default: [],
|
||||
mapStateToProps({ datasource, controls }, controlState) {
|
||||
const newState: ExtraControlProps = {};
|
||||
if (datasource) {
|
||||
if (datasource?.columns[0]?.hasOwnProperty('filterable')) {
|
||||
newState.options = (datasource as Dataset)?.columns?.filter(
|
||||
(c: ColumnMeta) => c.filterable,
|
||||
);
|
||||
} else {
|
||||
newState.options = datasource.columns;
|
||||
}
|
||||
}
|
||||
newState.queryMode = getQueryMode(controls);
|
||||
newState.externalValidationErrors =
|
||||
isRawMode({ controls }) &&
|
||||
ensureIsArray(controlState?.value).length === 0
|
||||
? [t('must have a value')]
|
||||
: [];
|
||||
return newState;
|
||||
},
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
} as typeof sharedControls.groupby,
|
||||
};
|
||||
|
||||
const metricsControlSetItem: ControlSetItem = {
|
||||
name: 'metrics',
|
||||
override: {
|
||||
validators: [],
|
||||
visibility: isAggMode,
|
||||
mapStateToProps: (
|
||||
{ controls, datasource, form_data }: ControlPanelState,
|
||||
controlState: ControlState,
|
||||
) => ({
|
||||
columns: datasource?.columns[0]?.hasOwnProperty('filterable')
|
||||
? (datasource as Dataset)?.columns?.filter(
|
||||
(c: ColumnMeta) => c.filterable,
|
||||
)
|
||||
: datasource?.columns,
|
||||
savedMetrics: defineSavedMetrics(datasource),
|
||||
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'],
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
const percentMetricsControlSetItem: ControlSetItem = {
|
||||
name: 'percent_metrics',
|
||||
config: {
|
||||
type: 'DndMetricSelect',
|
||||
label: t('Percentage metrics'),
|
||||
description: t(
|
||||
'Select one or many metrics to display, that will be displayed in the percentages of total. ' +
|
||||
'Percentage metrics will be calculated only from data within the row limit. ' +
|
||||
'You can use an aggregation function on a column or write custom SQL to create a percentage metric.',
|
||||
),
|
||||
multi: true,
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
mapStateToProps: ({ datasource, controls }, controlState) => ({
|
||||
columns: datasource?.columns || [],
|
||||
savedMetrics: defineSavedMetrics(datasource),
|
||||
datasource,
|
||||
datasourceType: datasource?.type,
|
||||
queryMode: getQueryMode(controls),
|
||||
externalValidationErrors: validateAggControlValues(controls, [
|
||||
controls.groupby?.value,
|
||||
controls.metrics?.value,
|
||||
controlState?.value,
|
||||
]),
|
||||
}),
|
||||
rerender: ['groupby', 'metrics'],
|
||||
default: [],
|
||||
validators: [],
|
||||
},
|
||||
};
|
||||
|
||||
const showTotalsControlSetItem: ControlSetItem = {
|
||||
name: 'show_totals',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show summary'),
|
||||
default: false,
|
||||
description: t(
|
||||
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
|
||||
),
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
const rowLimitControlSetItem: ControlSetItem = {
|
||||
name: 'row_limit',
|
||||
override: {
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
!controls?.server_pagination?.value,
|
||||
},
|
||||
};
|
||||
|
||||
const timeSeriesLimitMetricControlSetItem: ControlSetItem = {
|
||||
name: 'timeseries_limit_metric',
|
||||
override: {
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
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?.hasOwnProperty('order_by_choices')
|
||||
? (datasource as Dataset)?.order_by_choices
|
||||
: datasource?.columns || [],
|
||||
}),
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
const orderDescendingControlSetItem: ControlSetItem = {
|
||||
name: 'order_desc',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort descending'),
|
||||
default: true,
|
||||
description: t('Whether to sort descending or ascending'),
|
||||
visibility: ({ controls }) =>
|
||||
!!(
|
||||
isAggMode({ controls }) &&
|
||||
controls?.timeseries_limit_metric.value &&
|
||||
!isEmpty(controls?.timeseries_limit_metric.value)
|
||||
),
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Code Editor Controls ─────────────────────────────────────────────────────
|
||||
|
||||
const HandlebarsTemplateControl = (
|
||||
props: CustomControlConfig<{ value: string }>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const val = String(props?.value || props?.default || '');
|
||||
|
||||
const helperDescriptionsHeader = t(
|
||||
'Available Handlebars Helpers in Superset:',
|
||||
);
|
||||
const helperDescriptions = [
|
||||
{ key: 'dateFormat', descKey: 'Formats a date using a specified format.' },
|
||||
{ key: 'stringify', descKey: 'Converts an object to a JSON string.' },
|
||||
{
|
||||
key: 'formatNumber',
|
||||
descKey: 'Formats a number using locale-specific formatting.',
|
||||
},
|
||||
{
|
||||
key: 'parseJson',
|
||||
descKey: 'Parses a JSON string into a JavaScript object.',
|
||||
},
|
||||
];
|
||||
const helpersTooltipContent = `
|
||||
${helperDescriptionsHeader}
|
||||
|
||||
${helperDescriptions
|
||||
.map(({ key, descKey }) => `- **${key}**: ${t(descKey)}`)
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{props.label as ReactNode}
|
||||
<InfoTooltip
|
||||
iconStyle={{ marginLeft: theme.sizeUnit }}
|
||||
tooltip={<SafeMarkdown source={helpersTooltipContent} />}
|
||||
/>
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
value={val}
|
||||
onChange={source => {
|
||||
debounceFunc(props.onChange, source || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>{{stringify this}}</li>
|
||||
{{/each}}
|
||||
</ul>`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
validators: [validateNonEmpty],
|
||||
mapStateToProps: ({ controls }) => ({
|
||||
value: controls?.handlebars_template?.value,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const StyleControl = (props: CustomControlConfig<{ value: string }>) => {
|
||||
const theme = useTheme();
|
||||
const defaultValue = props?.value
|
||||
? undefined
|
||||
: `/*
|
||||
.data-list {
|
||||
background-color: yellow;
|
||||
}
|
||||
*/`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{props.label as ReactNode}
|
||||
<InfoTooltip
|
||||
iconStyle={{ marginLeft: theme.sizeUnit }}
|
||||
tooltip={t('You need to configure HTML sanitization to use CSS')}
|
||||
/>
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
mode="css"
|
||||
value={props.value}
|
||||
defaultValue={defaultValue}
|
||||
onChange={source => {
|
||||
debounceFunc(props.onChange, source || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styleControlSetItem: ControlSetItem = {
|
||||
name: 'styleTemplate',
|
||||
config: {
|
||||
...sharedControls.entity,
|
||||
type: StyleControl,
|
||||
label: t('CSS Styles'),
|
||||
description: t('CSS applied to the chart'),
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
validators: [],
|
||||
mapStateToProps: ({ controls }) => ({
|
||||
value: controls?.handlebars_template?.value,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Plugin Definition ────────────────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type HandlebarsExtra = Record<string, any>;
|
||||
|
||||
// Standalone exports for testing
|
||||
export function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
orderby: normalizeOrderBy(baseQueryObject).orderby,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function transformProps(chartProps: any) {
|
||||
const { formData, queriesData, width, height } = chartProps;
|
||||
const { data } = queriesData[0];
|
||||
return { width, height, data, formData };
|
||||
}
|
||||
|
||||
export default defineChart<Record<string, never>, HandlebarsExtra>({
|
||||
metadata: {
|
||||
name: t('Handlebars'),
|
||||
description: t('Write a handlebars template to render the data'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
},
|
||||
arguments: {},
|
||||
buildQuery: (formData: QueryFormData) =>
|
||||
buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
orderby: normalizeOrderBy(baseQueryObject).orderby,
|
||||
},
|
||||
]),
|
||||
suppressQuerySection: true,
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[queryModeControlSetItem],
|
||||
[groupByControlSetItem],
|
||||
[metricsControlSetItem, allColumnsControlSetItem],
|
||||
[percentMetricsControlSetItem],
|
||||
[timeSeriesLimitMetricControlSetItem, orderByControlSetItem],
|
||||
[orderDescendingControlSetItem],
|
||||
[rowLimitControlSetItem],
|
||||
[includeTimeControlSetItem],
|
||||
[showTotalsControlSetItem],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[handlebarsTemplateControlSetItem],
|
||||
[styleControlSetItem],
|
||||
],
|
||||
},
|
||||
],
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
render: (props: any) => <HandlebarsChart {...props} />,
|
||||
});
|
||||
@@ -1,32 +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 {
|
||||
buildQueryContext,
|
||||
normalizeOrderBy,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
orderby: normalizeOrderBy(baseQueryObject).orderby,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -1,78 +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 {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { allColumnsControlSetItem } from './controls/columns';
|
||||
import { groupByControlSetItem } from './controls/groupBy';
|
||||
import { handlebarsTemplateControlSetItem } from './controls/handlebarTemplate';
|
||||
import { includeTimeControlSetItem } from './controls/includeTime';
|
||||
import {
|
||||
rowLimitControlSetItem,
|
||||
timeSeriesLimitMetricControlSetItem,
|
||||
} from './controls/limits';
|
||||
import {
|
||||
metricsControlSetItem,
|
||||
percentMetricsControlSetItem,
|
||||
showTotalsControlSetItem,
|
||||
} from './controls/metrics';
|
||||
import {
|
||||
orderByControlSetItem,
|
||||
orderDescendingControlSetItem,
|
||||
} from './controls/orderBy';
|
||||
import { queryModeControlSetItem } from './controls/queryMode';
|
||||
import { styleControlSetItem } from './controls/style';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[queryModeControlSetItem],
|
||||
[groupByControlSetItem],
|
||||
[metricsControlSetItem, allColumnsControlSetItem],
|
||||
[percentMetricsControlSetItem],
|
||||
[timeSeriesLimitMetricControlSetItem, orderByControlSetItem],
|
||||
[orderDescendingControlSetItem],
|
||||
[rowLimitControlSetItem],
|
||||
[includeTimeControlSetItem],
|
||||
[showTotalsControlSetItem],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[handlebarsTemplateControlSetItem],
|
||||
[styleControlSetItem],
|
||||
],
|
||||
},
|
||||
],
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,58 +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 {
|
||||
ControlSetItem,
|
||||
ExtraControlProps,
|
||||
sharedControls,
|
||||
Dataset,
|
||||
ColumnMeta,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import { getQueryMode, isRawMode } from './shared';
|
||||
|
||||
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) {
|
||||
if (datasource?.columns[0]?.hasOwnProperty('filterable')) {
|
||||
newState.options = (datasource as Dataset)?.columns?.filter(
|
||||
(c: ColumnMeta) => c.filterable,
|
||||
);
|
||||
} else newState.options = datasource.columns;
|
||||
}
|
||||
newState.queryMode = getQueryMode(controls);
|
||||
newState.externalValidationErrors =
|
||||
isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0
|
||||
? [t('must have a value')]
|
||||
: [];
|
||||
return newState;
|
||||
},
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
};
|
||||
|
||||
export const allColumnsControlSetItem: ControlSetItem = {
|
||||
name: 'all_columns',
|
||||
config: dndAllColumns,
|
||||
};
|
||||
@@ -1,45 +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 {
|
||||
ControlPanelState,
|
||||
ControlSetItem,
|
||||
ControlState,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isAggMode, validateAggControlValues } from './shared';
|
||||
|
||||
export const groupByControlSetItem: ControlSetItem = {
|
||||
name: 'groupby',
|
||||
override: {
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
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'],
|
||||
},
|
||||
};
|
||||
@@ -1,98 +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 {
|
||||
ControlSetItem,
|
||||
CustomControlConfig,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { InfoTooltip } from '@superset-ui/core/components';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
|
||||
interface HandlebarsCustomControlProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const HandlebarsTemplateControl = (
|
||||
props: CustomControlConfig<HandlebarsCustomControlProps>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const val = String(
|
||||
props?.value ? props?.value : props?.default ? props?.default : '',
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{typeof props.label === 'function' ? null : props.label}
|
||||
<InfoTooltip
|
||||
iconStyle={{ marginLeft: theme.sizeUnit }}
|
||||
tooltip={
|
||||
<span>
|
||||
{t('See ')}{' '}
|
||||
<a
|
||||
href="https://superset.apache.org/docs/using-superset/handlebars-chart"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('the Handlebars chart documentation')}
|
||||
</a>{' '}
|
||||
{t('for a list of available helpers.')}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
value={val}
|
||||
onChange={source => {
|
||||
debounceFunc(props.onChange, 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>{{stringify this}}</li>
|
||||
{{/each}}
|
||||
</ul>`,
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
validators: [validateNonEmpty],
|
||||
mapStateToProps: ({ form_data }) => ({
|
||||
value: form_data?.handlebarsTemplate ?? form_data?.handlebars_template,
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -1,35 +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 { ControlSetItem } from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
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,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
@@ -1,39 +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 {
|
||||
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,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
@@ -1,113 +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 {
|
||||
ControlPanelState,
|
||||
ControlSetItem,
|
||||
ControlState,
|
||||
sharedControls,
|
||||
Dataset,
|
||||
ColumnMeta,
|
||||
defineSavedMetrics,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { getQueryMode, isAggMode, validateAggControlValues } from './shared';
|
||||
|
||||
const percentMetrics: typeof sharedControls.metrics = {
|
||||
type: 'MetricsControl',
|
||||
label: t('Percentage metrics'),
|
||||
description: t(
|
||||
'Select one or many metrics to display, that will be displayed in the percentages of total. ' +
|
||||
'Percentage metrics will be calculated only from data within the row limit. ' +
|
||||
'You can use an aggregation function on a column or write custom SQL to create a percentage metric.',
|
||||
),
|
||||
multi: true,
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
mapStateToProps: ({ datasource, controls }, controlState) => ({
|
||||
columns: datasource?.columns || [],
|
||||
savedMetrics: defineSavedMetrics(datasource),
|
||||
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: {
|
||||
...dndPercentMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
export const metricsControlSetItem: ControlSetItem = {
|
||||
name: 'metrics',
|
||||
override: {
|
||||
validators: [],
|
||||
visibility: isAggMode,
|
||||
mapStateToProps: (
|
||||
{ controls, datasource, form_data }: ControlPanelState,
|
||||
controlState: ControlState,
|
||||
) => ({
|
||||
columns: datasource?.columns[0]?.hasOwnProperty('filterable')
|
||||
? (datasource as Dataset)?.columns?.filter(
|
||||
(c: ColumnMeta) => c.filterable,
|
||||
)
|
||||
: datasource?.columns,
|
||||
savedMetrics: defineSavedMetrics(datasource),
|
||||
// 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'],
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const showTotalsControlSetItem: ControlSetItem = {
|
||||
name: 'show_totals',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show summary'),
|
||||
default: false,
|
||||
description: t(
|
||||
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
|
||||
),
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
@@ -1,57 +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 { ControlSetItem, Dataset } from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { isEmpty } from 'lodash';
|
||||
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?.hasOwnProperty('order_by_choices')
|
||||
? (datasource as Dataset)?.order_by_choices
|
||||
: datasource?.columns || [],
|
||||
}),
|
||||
visibility: isRawMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
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: ({ controls }) =>
|
||||
!!(
|
||||
isAggMode({ controls }) &&
|
||||
controls?.timeseries_limit_metric.value &&
|
||||
!isEmpty(controls?.timeseries_limit_metric.value)
|
||||
),
|
||||
resetOnHide: false,
|
||||
},
|
||||
};
|
||||
@@ -1,43 +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 {
|
||||
ControlConfig,
|
||||
ControlSetItem,
|
||||
QueryModeLabel,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { QueryMode } 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,
|
||||
};
|
||||
@@ -1,57 +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 {
|
||||
ControlPanelsContainerProps,
|
||||
ControlStateMapping,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray, QueryFormColumn, QueryMode } 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')]
|
||||
: [];
|
||||
};
|
||||
@@ -1,95 +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 {
|
||||
ControlSetItem,
|
||||
CustomControlConfig,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { InfoTooltip } from '@superset-ui/core/components';
|
||||
import { CodeEditor } from '../../components/CodeEditor/CodeEditor';
|
||||
import { ControlHeader } from '../../components/ControlHeader/controlHeader';
|
||||
import { debounceFunc } from '../../consts';
|
||||
|
||||
interface StyleCustomControlProps {
|
||||
value: string;
|
||||
htmlSanitization: boolean;
|
||||
}
|
||||
|
||||
const StyleControl = (props: CustomControlConfig<StyleCustomControlProps>) => {
|
||||
const theme = useTheme();
|
||||
const htmlSanitization = props.htmlSanitization ?? true;
|
||||
|
||||
const defaultValue = props?.value
|
||||
? undefined
|
||||
: `/*
|
||||
.data-list {
|
||||
background-color: yellow;
|
||||
}
|
||||
*/`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader>
|
||||
<div>
|
||||
{typeof props.label === 'function' ? null : props.label}
|
||||
{htmlSanitization && (
|
||||
<InfoTooltip
|
||||
iconStyle={{ marginLeft: theme.sizeUnit }}
|
||||
tooltip={t(
|
||||
'CSS styles may be removed by server-side HTML sanitization. ' +
|
||||
'If styles are not applying, ask your Superset administrator ' +
|
||||
'to adjust the HTML sanitization configuration.',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ControlHeader>
|
||||
<CodeEditor
|
||||
theme="dark"
|
||||
mode="css"
|
||||
value={props.value}
|
||||
defaultValue={defaultValue}
|
||||
onChange={source => {
|
||||
debounceFunc(props.onChange, source || '');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const styleControlSetItem: ControlSetItem = {
|
||||
name: 'styleTemplate',
|
||||
config: {
|
||||
...sharedControls.entity,
|
||||
type: StyleControl,
|
||||
label: t('CSS Styles'),
|
||||
description: t('CSS applied to the chart'),
|
||||
isInt: false,
|
||||
renderTrigger: true,
|
||||
valueKey: null,
|
||||
|
||||
validators: [],
|
||||
mapStateToProps: ({ form_data, common }) => ({
|
||||
value: form_data?.styleTemplate ?? form_data?.style_template,
|
||||
htmlSanitization: common?.conf?.HTML_SANITIZATION ?? true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -1,62 +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 { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import thumbnail from '../images/thumbnail.png';
|
||||
import thumbnailDark from '../images/thumbnail-dark.png';
|
||||
import example1 from '../images/example1.jpg';
|
||||
import example1Dark from '../images/example1-dark.jpg';
|
||||
import example2 from '../images/example2.jpg';
|
||||
import example2Dark from '../images/example2-dark.jpg';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
|
||||
export default class HandlebarsChartPlugin extends ChartPlugin {
|
||||
/**
|
||||
* The constructor is used to pass relevant metadata and callbacks that get
|
||||
* registered in respective registries that are used throughout the library
|
||||
* and application. A more thorough description of each property is given in
|
||||
* the respective imported file.
|
||||
*
|
||||
* It is worth noting that `buildQuery` and is optional, and only needed for
|
||||
* advanced visualizations that require either post processing operations
|
||||
* (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
|
||||
*/
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
description: t('Write a handlebars template to render the data'),
|
||||
name: t('Handlebars'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [
|
||||
{ url: example1, urlDark: example1Dark },
|
||||
{ url: example2, urlDark: example2Dark },
|
||||
],
|
||||
});
|
||||
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('../Handlebars'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,31 +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 { ChartProps, TimeseriesDataRecord } from '@superset-ui/core';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const data = queriesData[0].data as TimeseriesDataRecord[];
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
formData,
|
||||
};
|
||||
}
|
||||
@@ -1,268 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core';
|
||||
import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
|
||||
import { kpiData, leaderboardData, timelineData } from './data';
|
||||
import { withResizableChartDemo } from '@storybook-shared';
|
||||
|
||||
const VIZ_TYPE = 'handlebars';
|
||||
|
||||
new HandlebarsChartPlugin().configure({ key: VIZ_TYPE }).register();
|
||||
|
||||
getChartTransformPropsRegistry().registerValue(
|
||||
VIZ_TYPE,
|
||||
(chartProps: {
|
||||
width: number;
|
||||
height: number;
|
||||
formData: object;
|
||||
queriesData: { data: unknown[] }[];
|
||||
}) => {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { data } = queriesData[0];
|
||||
return { width, height, data, formData };
|
||||
},
|
||||
);
|
||||
|
||||
// KPI Dashboard template - uses inline styles for Storybook compatibility
|
||||
const kpiTemplate = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
|
||||
{{#each data}}
|
||||
<div style="background: linear-gradient(135deg, {{#if (eq status 'success')}}#11998e 0%, #38ef7d{{else}}{{#if (eq status 'warning')}}#f093fb 0%, #f5576c{{else}}#667eea 0%, #764ba2{{/if}}{{/if}} 100%); border-radius: 16px; padding: 20px; color: white; box-shadow: 0 10px 40px rgba(0,0,0,0.2);">
|
||||
<div style="font-size: 32px; margin-bottom: 12px;">{{icon}}</div>
|
||||
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 1px; opacity: 0.9;">{{metric}}</div>
|
||||
<div style="font-size: 32px; font-weight: 700; margin: 4px 0;">{{formatNumber value}}</div>
|
||||
<div style="display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; background: {{#if (gt change 0)}}rgba(255,255,255,0.2){{else}}rgba(0,0,0,0.15){{/if}};">
|
||||
{{#if (gt change 0)}}<span>▲</span>{{else}}<span>▼</span>{{/if}}
|
||||
{{change}}%
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 11px; opacity: 0.8;">Target: {{formatNumber target}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Leaderboard template - dark theme with inline styles
|
||||
const leaderboardTemplate = `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; border-radius: 16px; padding: 24px; color: #eee;">
|
||||
<h2 style="margin: 0 0 20px 0; font-size: 18px; color: #fff;">🏆 Top Performers</h2>
|
||||
{{#each data}}
|
||||
<div style="display: flex; align-items: center; padding: 12px 16px; margin: 8px 0; background: {{#if (eq rank 1)}}linear-gradient(90deg, rgba(255,215,0,0.2) 0%, transparent 100%){{else}}{{#if (eq rank 2)}}linear-gradient(90deg, rgba(192,192,192,0.2) 0%, transparent 100%){{else}}{{#if (eq rank 3)}}linear-gradient(90deg, rgba(205,127,50,0.2) 0%, transparent 100%){{else}}rgba(255,255,255,0.05){{/if}}{{/if}}{{/if}}; border-radius: 12px; {{#if (eq rank 1)}}border-left: 3px solid #ffd700;{{/if}}{{#if (eq rank 2)}}border-left: 3px solid #c0c0c0;{{/if}}{{#if (eq rank 3)}}border-left: 3px solid #cd7f32;{{/if}}">
|
||||
<div style="width: 28px; height: 28px; border-radius: 50%; background: {{#if (eq rank 1)}}#ffd700{{else}}{{#if (eq rank 2)}}#c0c0c0{{else}}{{#if (eq rank 3)}}#cd7f32{{else}}rgba(255,255,255,0.1){{/if}}{{/if}}{{/if}}; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; margin-right: 12px; {{#if (lte rank 3)}}color: #1a1a2e;{{/if}}">{{rank}}</div>
|
||||
<div style="font-size: 28px; margin-right: 12px;">{{avatar}}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; font-size: 14px;">{{name}}</div>
|
||||
<div style="font-size: 11px; color: #888; margin-top: 2px;">{{team}}</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 18px; font-weight: 700; color: #fff;">{{formatNumber score}}</div>
|
||||
<div style="font-size: 12px; margin-top: 2px; color: {{#if (eq trend 'up')}}#38ef7d{{else}}{{#if (eq trend 'down')}}#f5576c{{else}}#888{{/if}}{{/if}};">
|
||||
{{#if (eq trend 'up')}}↑{{/if}}{{#if (eq trend 'down')}}↓{{/if}}{{#if (eq trend 'same')}}→{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Timeline template with inline styles
|
||||
const timelineTemplate = `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; position: relative; padding-left: 40px;">
|
||||
<div style="position: absolute; left: 13px; top: 8px; bottom: 8px; width: 3px; background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); border-radius: 2px;"></div>
|
||||
{{#each data}}
|
||||
<div style="position: relative; padding: 12px 0;">
|
||||
<div style="position: absolute; left: -33px; top: 16px; width: 16px; height: 16px; border-radius: 50%; background: {{#if (eq type 'milestone')}}#ffd700{{else}}{{#if (eq type 'success')}}#38ef7d{{else}}{{#if (eq type 'warning')}}#f5576c{{else}}#667eea{{/if}}{{/if}}{{/if}}; border: 3px solid #fff; box-shadow: 0 0 0 3px {{#if (eq type 'milestone')}}rgba(255,215,0,0.3){{else}}{{#if (eq type 'success')}}rgba(56,239,125,0.3){{else}}{{#if (eq type 'warning')}}rgba(245,87,108,0.3){{else}}rgba(102,126,234,0.3){{/if}}{{/if}}{{/if}};"></div>
|
||||
<div style="background: #f8f9fa; border-radius: 12px; padding: 16px 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); margin-left: 8px;">
|
||||
<div style="font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">{{date}}</div>
|
||||
<div style="font-size: 16px; font-weight: 600; color: #333; margin: 4px 0;">{{event}}</div>
|
||||
<div style="font-size: 13px; color: #666; line-height: 1.4;">{{description}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Simple editable template for the interactive demo
|
||||
const simpleTemplate = `<div style="font-family: sans-serif; padding: 16px;">
|
||||
<h2 style="margin: 0 0 16px 0;">{{title}}</h2>
|
||||
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||
{{#each data}}
|
||||
<li style="padding: 8px; margin: 4px 0; background: #f5f5f5; border-radius: 4px;">
|
||||
<strong>{{metric}}</strong>: {{formatNumber value}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
// Simple CSS for the interactive demo
|
||||
const simpleCSS = `.handlebars-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.handlebars-container h2 {
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.handlebars-container ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.handlebars-container li {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
}`;
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/plugin-chart-handlebars',
|
||||
decorators: [withResizableChartDemo],
|
||||
};
|
||||
|
||||
export const InteractiveHandlebars = ({
|
||||
handlebarsTemplate,
|
||||
styleTemplate,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
handlebarsTemplate: string;
|
||||
styleTemplate: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VIZ_TYPE}
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data: kpiData }]}
|
||||
formData={{
|
||||
datasource: '1__table',
|
||||
viz_type: VIZ_TYPE,
|
||||
handlebars_template: handlebarsTemplate,
|
||||
style_template: styleTemplate,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
InteractiveHandlebars.args = {
|
||||
handlebarsTemplate: simpleTemplate,
|
||||
styleTemplate: simpleCSS,
|
||||
};
|
||||
|
||||
InteractiveHandlebars.argTypes = {
|
||||
handlebarsTemplate: {
|
||||
control: { type: 'text' },
|
||||
description: 'Handlebars template for rendering data',
|
||||
},
|
||||
styleTemplate: {
|
||||
control: { type: 'text' },
|
||||
description: 'CSS styles to apply to the chart',
|
||||
},
|
||||
};
|
||||
|
||||
InteractiveHandlebars.parameters = {
|
||||
initialSize: {
|
||||
width: 600,
|
||||
height: 400,
|
||||
},
|
||||
};
|
||||
|
||||
export const KPIDashboard = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VIZ_TYPE}
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data: kpiData }]}
|
||||
formData={{
|
||||
datasource: '1__table',
|
||||
viz_type: VIZ_TYPE,
|
||||
handlebars_template: kpiTemplate,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
KPIDashboard.parameters = {
|
||||
initialSize: {
|
||||
width: 900,
|
||||
height: 280,
|
||||
},
|
||||
};
|
||||
|
||||
export const Leaderboard = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VIZ_TYPE}
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data: leaderboardData }]}
|
||||
formData={{
|
||||
datasource: '1__table',
|
||||
viz_type: VIZ_TYPE,
|
||||
handlebars_template: leaderboardTemplate,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Leaderboard.parameters = {
|
||||
initialSize: {
|
||||
width: 450,
|
||||
height: 420,
|
||||
},
|
||||
};
|
||||
|
||||
export const Timeline = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VIZ_TYPE}
|
||||
width={width}
|
||||
height={height}
|
||||
queriesData={[{ data: timelineData }]}
|
||||
formData={{
|
||||
datasource: '1__table',
|
||||
viz_type: VIZ_TYPE,
|
||||
handlebars_template: timelineTemplate,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Timeline.parameters = {
|
||||
initialSize: {
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
};
|
||||
@@ -1,62 +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 {
|
||||
QueryFormData,
|
||||
QueryFormMetric,
|
||||
QueryMode,
|
||||
TimeGranularity,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
export interface HandlebarsStylesProps {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface HandlebarsCustomizeProps {
|
||||
handlebarsTemplate?: string;
|
||||
styleTemplate?: string;
|
||||
}
|
||||
|
||||
export type HandlebarsQueryFormData = QueryFormData &
|
||||
HandlebarsStylesProps &
|
||||
HandlebarsCustomizeProps & {
|
||||
align_pn?: boolean;
|
||||
color_pn?: boolean;
|
||||
include_time?: boolean;
|
||||
include_search?: boolean;
|
||||
query_mode?: QueryMode;
|
||||
page_length?: string | number | null; // null means auto-paginate
|
||||
metrics?: QueryFormMetric[] | null;
|
||||
percent_metrics?: QueryFormMetric[] | null;
|
||||
timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null;
|
||||
groupby?: QueryFormMetric[] | null;
|
||||
all_columns?: QueryFormMetric[] | null;
|
||||
order_desc?: boolean;
|
||||
table_timestamp_format?: string;
|
||||
granularitySqla?: string;
|
||||
time_grain_sqla?: TimeGranularity;
|
||||
};
|
||||
|
||||
export type HandlebarsProps = HandlebarsStylesProps &
|
||||
HandlebarsCustomizeProps & {
|
||||
data: TimeseriesDataRecord[];
|
||||
// add typing here for the props you pass in from transformProps.ts!
|
||||
formData: HandlebarsQueryFormData;
|
||||
};
|
||||
Reference in New Issue
Block a user