Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Li
dc0de0e248 fix(embedded): respect show_filters param in standalone report mode
standalone=3 (Report mode) unconditionally hid the filter bar, breaking
embedded dashboards that set dashboardUIConfig.filters.visible = true.
Now show_filters=true overrides the Report mode hiding while preserving
the default behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:51:35 -07:00
Joe Li
4b7283e3b8 fix(pivot-table): guard convertToNumberIfNumeric against non-string Date values
When temporal columns are used in pivot table Rows/Columns, Date objects
can reach convertToNumberIfNumeric at runtime despite the string[] type
declaration (due to any-typed boundaries in the data pipeline). Calling
.trim() on a Date object throws TypeError: t.trim is not a function,
crashing the chart entirely.

Widen the parameter type to unknown and add a type guard that returns
non-string values unchanged, letting the date formatter receive them
directly. Also update makeColPivotSettings test helper to accept unknown
and add regression tests for the Date object passthrough paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 09:51:49 -07:00
22 changed files with 254 additions and 657 deletions

View File

@@ -1,122 +0,0 @@
---
title: Glossary
hide_title: true
sidebar_position: 10
---
import { getAllGlossaryTopics } from '../../superset-frontend/packages/superset-ui-core/src/glossary';
import { Table, ConfigProvider, theme } from 'antd';
import { useColorMode } from '@docusaurus/theme-common';
import { useCallback, useEffect, useRef } from 'react';
export const GlossaryStructure = [
{
title: 'Term',
dataIndex: 'title',
key: 'title',
width: 200,
},
{
title: 'Short Description',
dataIndex: 'short',
key: 'short',
},
];
export const GlossaryContent = () => {
const { colorMode } = useColorMode();
const isDark = colorMode === 'dark';
const tableRefs = useRef({});
const scrollToRow = useCallback((topic, rowKey) => {
const topicId = encodeURIComponent(topic);
const encRowKey = encodeURIComponent(rowKey);
const row = tableRefs.current[topicId]?.[encRowKey];
if (row) {
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
row.classList.add('table-row-highlight');
setTimeout(() => row.classList.remove('table-row-highlight'), 2000);
}
}, []);
useEffect(() => {
let hash = '';
try {
hash = decodeURIComponent(window.location.hash.slice(1));
} catch (e) {
// Malformed percent-encoding in the URL hash — silently skip the
// scroll-to-row behavior rather than letting the page render fail.
return;
}
if (!hash) return;
const [topic, term] = hash.split('__');
if (topic && term) scrollToRow(topic, hash);
}, [scrollToRow]);
return (
<div>
<ConfigProvider
theme={{
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
}}
>
{getAllGlossaryTopics().map((topic) => {
const topicName = topic.getName();
const topicFragment = encodeURIComponent(topicName);
const terms = topic.getAllTerms();
return (
<div key={topicName} id={topicFragment}>
<h3>{topic.getDisplayName()}</h3>
<Table
dataSource={terms
.map((term) => {
const key = term.getTitle()
? encodeURIComponent(`${topicName}__${term.getTitle()}`)
: undefined;
return key
? {
title: term.getDisplayTitle(),
short: term.getShort(),
key,
}
: null;
})
.filter(Boolean)}
columns={GlossaryStructure}
rowKey="key"
pagination={false}
showHeader
bordered
onRow={(record) => {
if (!record?.key) return {};
const topicId = topicFragment;
return {
ref: (node) => {
if (!tableRefs.current[topicId]) tableRefs.current[topicId] = {};
if (node) {
tableRefs.current[topicId][record.key] = node;
} else {
// cleanup stale reference when row unmounts
delete tableRefs.current[topicId][record.key];
if (Object.keys(tableRefs.current[topicId]).length === 0) {
delete tableRefs.current[topicId];
}
}
},
};
}}
/>
</div>
);
})}
</ConfigProvider>
</div>
);
};
## Glossary
<GlossaryContent />

View File

@@ -60,11 +60,6 @@ const sidebars = {
},
],
},
{
type: 'doc',
label: 'Glossary',
id: 'glossary'
},
{
type: 'doc',
label: 'FAQ',

View File

@@ -23,10 +23,6 @@ import { ControlSubSectionHeader } from '../components/ControlSubSectionHeader';
import { ControlPanelSectionConfig } from '../types';
import { formatSelectOptions, displayTimeRelatedControls } from '../utils';
import { glossary } from '@superset-ui/core';
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
export const advancedAnalyticsControls: ControlPanelSectionConfig = {
label: t('Advanced analytics'),
tabOverride: 'data',
@@ -127,7 +123,12 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
['156 weeks ago', t('156 weeks ago')],
['3 years ago', t('3 years ago')],
],
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay one or more timeseries from a ' +
'relative time period. Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported.',
),
},
},
],

View File

@@ -25,10 +25,6 @@ import {
ControlState,
} from '../types';
import { INVALID_DATE } from '..';
import { glossary } from '@superset-ui/core';
// Glossary terms used for tooltips
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
const fullChoices = [
['1 day ago', t('1 day ago')],
@@ -86,7 +82,16 @@ export const timeComparisonControls: ({
placeholder: t('Select or type a custom value...'),
label: t('Time shift'),
choices: showFullChoices ? fullChoices : reducedChoices,
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay results from a relative time period. ' +
'Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported. ' +
'Use "Inherit range from time filters" ' +
'to shift the comparison time range ' +
'by the same length as your time range ' +
'and use "Custom" to set a custom comparison range.',
),
},
},
],

View File

@@ -39,13 +39,6 @@ import {
xAxisMixin,
} from '..';
import { glossary } from '@superset-ui/core';
// Glossary terms used for tooltips
const DIMENSION_DESCRIPTION = glossary.Query.Dimension.encode();
const METRIC_DESCRIPTION = glossary.Query.Metric.encode();
const SORT_DESCRIPTION = glossary.Query.Sort.encode();
type Control = {
savedMetrics?: Metric[] | null;
default?: unknown;
@@ -85,7 +78,11 @@ export const dndGroupByControl: SharedControlConfig<
clearable: true,
default: [],
includeTime: false,
description: DIMENSION_DESCRIPTION,
description: t(
'Dimensions contain qualitative values such as names, dates, or geographical data. ' +
'Use dimensions to categorize, segment, and reveal the details in your data. ' +
'Dimensions affect the level of detail in the view.',
),
optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
valueKey: 'column_name',
@@ -183,7 +180,11 @@ export const dndAdhocMetricsControl: SharedControlConfig<
datasource,
datasourceType: datasource?.type,
}),
description: METRIC_DESCRIPTION,
description: t(
'Select one or many metrics to display. ' +
'You can use an aggregation function on a column ' +
'or write custom SQL to create a metric.',
),
};
export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
@@ -223,7 +224,11 @@ export const dndSortByControl: SharedControlConfig<
type: 'DndMetricSelect',
label: t('Sort query by'),
default: null,
description: SORT_DESCRIPTION,
description: t(
'Orders the query result that generates the source data for this chart. ' +
'If a series or row limit is reached, this determines what data are truncated. ' +
'If undefined, defaults to the first metric (where appropriate).',
),
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),

View File

@@ -86,10 +86,6 @@ import {
dndTooltipMetricsControl,
} from './dndControls';
import { matrixifyControls } from './matrixifyControls';
import { glossary } from '@superset-ui/core';
const SERIES_DESCRIPTION = glossary.Query.Series.encode();
const ROW_LIMIT_DESCRIPTION = glossary.Query.Row_Limit.encode();
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const sequentialSchemeRegistry = getSequentialSchemeRegistry();
@@ -239,7 +235,9 @@ const row_limit: SharedControlConfig<'SelectControl'> = {
],
default: 10000,
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
description: ROW_LIMIT_DESCRIPTION,
description: t(
'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
),
};
const order_desc: SharedControlConfig<'CheckboxControl'> = {
@@ -264,7 +262,12 @@ const limit: SharedControlConfig<'SelectControl'> = {
validators: [legacyValidateInteger],
choices: formatSelectOptions(SERIES_LIMITS),
clearable: true,
description: SERIES_DESCRIPTION,
description: t(
'Limits the number of series that get displayed. A joined subquery (or an extra phase ' +
'where subqueries are not supported) is applied to limit the number of series that get ' +
'fetched and rendered. This feature is useful when grouping by high cardinality ' +
'column(s) though does increase the query complexity and cost.',
),
};
const series_limit: SharedControlConfig<'SelectControl'> = {
@@ -274,7 +277,12 @@ const series_limit: SharedControlConfig<'SelectControl'> = {
placeholder: t('None'),
validators: [legacyValidateInteger],
choices: formatSelectOptions(SERIES_LIMITS),
description: SERIES_DESCRIPTION,
description: t(
'Limits the number of series that get displayed. A joined subquery (or an extra phase ' +
'where subqueries are not supported) is applied to limit the number of series that get ' +
'fetched and rendered. This feature is useful when grouping by high cardinality ' +
'column(s) though does increase the query complexity and cost.',
),
};
const group_others_when_limit_reached: SharedControlConfig<'CheckboxControl'> =

View File

@@ -16,70 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { CSSProperties } from 'react';
import { Tooltip as AntdTooltip } from 'antd';
import type { TooltipProps, TooltipPlacement } from './types';
import { resolveGlossaryString } from '@superset-ui/core';
const TOOLTIP_SEPARATOR_STYLE: CSSProperties = {
margin: '8px 0',
border: 'none',
borderTop: '1px solid rgba(255, 255, 255, 0.2)',
};
export const Tooltip = ({
overlayStyle,
title,
children,
...props
}: TooltipProps) => {
if (typeof title !== 'string') {
return (
<AntdTooltip
title={title}
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
>
{children}
</AntdTooltip>
);
}
const [glossaryUrl, description] = resolveGlossaryString(title);
const wrappedChildren = glossaryUrl ? (
<a href={glossaryUrl} target="_blank" rel="noopener noreferrer">
{children}
</a>
) : (
children
);
const wrappedDescription = glossaryUrl ? (
<>
{description}
<hr style={TOOLTIP_SEPARATOR_STYLE} />
<em>Click to Learn More</em>
</>
) : (
description
);
return (
<AntdTooltip
title={wrappedDescription}
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
>
{wrappedChildren}
</AntdTooltip>
);
};
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
<AntdTooltip
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
/>
);
export type { TooltipProps, TooltipPlacement };

View File

@@ -1,121 +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.
*/
/**
* Glossary definition containing terms organized by topic.
*
* ## How to add new glossary entries:
*
* 1. Add a new topic (if needed) or use an existing one
* 2. Add a term under the topic with a key (term name) and value object containing:
* - short: A brief description (displayed in tooltips)
* - extended (optional): An extended description (displayed in documentation)
*
* ## Example:
* export const glossaryDefinition: GlossaryDefinition = {
* Query: {
* Row_Limit: {
* short: noTranslate('Limits the number of rows...'),
* extended: noTranslate('Additional details...'), // optional
* },
* },
* };
*
* ## Formatting Notes:
* - Term names with underscores (e.g., `Row_Limit`) will be displayed with spaces
* (e.g., "Row Limit") when rendered in the UI and documentation
*/
export const glossaryDefinition: GlossaryDefinition = {
Query: {
Dimension: {
short: noTranslate(
'Dimensions contain qualitative values such as names, dates, or geographical data. ' +
'Use dimensions to categorize, segment, and reveal the details in your data. ' +
'Dimensions affect the level of detail in the view.',
),
},
Metric: {
short: noTranslate(
'Select one or many metrics to display. ' +
'You can use an aggregation function on a column or write custom SQL to create a metric.',
),
},
Series: {
short: noTranslate(
'Limits the number of series that get displayed. ' +
'A joined subquery (or an extra phase where subqueries are not supported) is applied ' +
'to limit the number of series that get fetched and rendered. ' +
'This feature is useful when grouping by high cardinality column(s) ' +
'though does increase the query complexity and cost.',
),
},
Row_Limit: {
short: noTranslate(
'Limits the number of rows that get displayed. ' +
'This feature is useful when grouping by high cardinality column(s) ' +
'though does increase the query complexity and cost.',
),
},
Sort: {
short: noTranslate(
'Orders the query result that generates the source data for this chart. ' +
'If a series or row limit is reached, this determines what data are truncated. ' +
'If undefined, defaults to the first metric (where appropriate).',
),
},
},
Advanced_Analytics: {
Time_Shift: {
short: noTranslate(
'Overlay results from a relative time period. ' +
'Expects relative time deltas in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported. ' +
'Use "Inherit range from time filters" to shift the comparison time range ' +
'by the same length as your time range and use "Custom" to set a custom comparison range.',
),
},
},
};
/**
* Identity passthrough used in environments (such as the docs site) that do
* not have an i18n runtime. Translation of glossary strings is performed at
* resolution time by callers in app contexts that do have i18n available.
*
* Named `noTranslate` (rather than `t`) so it does not visually shadow the
* imported i18n `t` used elsewhere in this package.
*/
function noTranslate(message: string): string {
return message;
}
/**
* The glossary definition is a nested object where the first level keys are topics,
* and the second level keys are term titles. This remains a static string-based
* structure, mainly for good IDE autocomplete.
*/
export type GlossaryStrings = {
short: string;
extended?: string;
};
export type GlossaryDefinition = Record<
string,
Record<string, GlossaryStrings>
>;

View File

@@ -1,154 +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.
*/
// Local type definition to avoid circular dependency with glossaryUtils
type Glossary = Record<string, Record<string, GlossaryTerm>>;
// Encoding format prefix for glossary strings
export const GLOSSARY_ENCODING_PREFIX = '[GLOSSARY]|';
export class GlossaryTerm {
/**
* The topic under which the term is categorized.
*/
private readonly topic: string;
/**
* The name of the term being defined.
*/
private readonly title: string;
/**
* A short description of the term. Displayed on the frontend as a tooltip.
*/
private readonly short: string;
/**
* An extended description of the term, shown alongside short on the documentation.
*/
private readonly extended?: string;
constructor(options: {
topic: string;
title: string;
short: string;
extended?: string;
}) {
this.topic = options.topic;
this.title = options.title;
this.short = options.short;
this.extended = options.extended;
}
getTopic(): string {
return this.topic;
}
getTitle(): string {
return this.title;
}
/**
* Returns a formatted display version of the title with underscores replaced by spaces.
*/
getDisplayTitle(): string {
return this.title.replace(/_/g, ' ');
}
/**
* Returns the short description, optionally transformed by a provided translation function.
*/
getShort(t?: (value: string) => string): string {
if (!t) {
return this.short;
}
return t(this.short);
}
getExtended(t?: (value: string) => string): string | undefined {
if (!t) {
return this.extended;
}
if (!this.extended) {
return undefined;
}
return t(this.extended);
}
/**
* Encodes the glossary term into a string format that can be resolved later.
* Format: [GLOSSARY]|topic|title
*/
encode(): string {
return `${GLOSSARY_ENCODING_PREFIX}${this.topic}|${this.title}`;
}
}
export class GlossaryTopic {
private readonly name: string;
private readonly terms: Map<string, GlossaryTerm>;
constructor(name: string, terms: GlossaryTerm[]) {
this.name = name;
this.terms = new Map(terms.map(term => [term.getTitle(), term]));
}
getName(): string {
return this.name;
}
/**
* Returns a formatted display version of the topic name with underscores replaced by spaces.
*/
getDisplayName(): string {
return this.name.replace(/_/g, ' ');
}
getTerm(title: string): GlossaryTerm | undefined {
return this.terms.get(title);
}
getAllTerms(): GlossaryTerm[] {
return Array.from(this.terms.values());
}
}
export class GlossaryMap {
private readonly topics: Map<string, GlossaryTopic>;
constructor(glossary: Glossary) {
const topics = new Map<string, GlossaryTopic>();
Object.entries(glossary).forEach(([topicName, termsByTitle]) => {
const topicTerms = Object.values(termsByTitle);
topics.set(topicName, new GlossaryTopic(topicName, topicTerms));
});
this.topics = topics;
}
getTopic(topicName: string): GlossaryTopic | undefined {
return this.topics.get(topicName);
}
getAllTopics(): GlossaryTopic[] {
return Array.from(this.topics.values());
}
}

View File

@@ -1,63 +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 {
GlossaryMap,
GlossaryTerm,
type GlossaryTopic,
} from './glossaryModels';
import { glossaryDefinition } from './glossary';
/**
* The exported glossary object is a runtime structure where each entry is a GlossaryTerm instance, but the key
* structure mirrors `glossaryDefinition` so IDEs can autocomplete, yet callers can use methods like `getShort()`.
*/
export type Glossary = {
[Topic in keyof typeof glossaryDefinition]: {
[Title in keyof (typeof glossaryDefinition)[Topic]]: GlossaryTerm;
};
};
const glossary: Glossary = Object.fromEntries(
Object.entries(glossaryDefinition).map(([topic, termsByTitle]) => [
topic,
Object.fromEntries(
Object.entries(termsByTitle).map(([title, termStrings]) => [
title,
new GlossaryTerm({
topic,
title,
short: termStrings.short,
extended: termStrings.extended ?? '',
}),
]),
),
]),
) as Glossary;
const glossaryMap = new GlossaryMap(glossary);
export const getAllGlossaryTopics = (): GlossaryTopic[] =>
glossaryMap.getAllTopics();
export const getGlossaryTopic = (
topicName: string,
): GlossaryTopic | undefined => glossaryMap.getTopic(topicName);
export default glossary;

View File

@@ -1,26 +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 { GlossaryTerm, GlossaryTopic } from './glossaryModels';
export {
default as glossary,
getAllGlossaryTopics,
getGlossaryTopic,
} from './glossaryUtils';
export { resolveGlossaryString } from './tooltipUtils';

View File

@@ -1,50 +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 { getGlossaryTopic } from './glossaryUtils';
import { t } from '@superset-ui/core';
export const GLOSSARY_BASE_URL = 'https://superset.apache.org/docs';
// Pattern matches: [GLOSSARY]|topic|title
// Captures: topic and title for lookup in glossary
const GLOSSARY_ENCODING_PATTERN = /^\[GLOSSARY\]\|([^|]+)\|([^|]+)$/;
export const resolveGlossaryString = (
glossaryString: string,
): [string | undefined, string] => {
const encoded = glossaryString.trim();
const match = encoded.match(GLOSSARY_ENCODING_PATTERN);
if (!match) {
return [undefined, encoded];
}
const topic = match[1];
const title = match[2];
// Look up the term from the glossary to get the translated description
const glossaryTopic = getGlossaryTopic(topic);
const term = glossaryTopic?.getTerm(title);
const description = term ? term.getShort(t) : encoded;
const glossaryUrl = buildGlossaryUrl(topic, title);
return [glossaryUrl, description];
};
const buildGlossaryUrl = (topic: string, title: string): string =>
`${GLOSSARY_BASE_URL}/glossary#${encodeURIComponent(`${topic}__${title}`)}`;

View File

@@ -35,4 +35,3 @@ export * from './ui-overrides';
export * from './hooks';
export * from './currency-format';
export * from './time-comparison';
export * from './glossary';

View File

@@ -28,9 +28,6 @@ import {
getStandardizedControls,
} from '@superset-ui/chart-controls';
import OptionDescription from './OptionDescription';
import { glossary } from '@superset-ui/core';
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -324,7 +321,12 @@ const config: ControlPanelConfig = {
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay one or more timeseries from a ' +
'relative time period. Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported.',
),
},
},
{

View File

@@ -26,9 +26,6 @@ import {
sections,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { glossary } from '@superset-ui/core';
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -207,7 +204,12 @@ const config: ControlPanelConfig = {
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay one or more timeseries from a ' +
'relative time period. Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported.',
),
},
},
{

View File

@@ -28,10 +28,6 @@ import {
D3_FORMAT_OPTIONS,
} from '@superset-ui/chart-controls';
import { glossary } from '@superset-ui/core';
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
/*
Plugins in question:
@@ -476,7 +472,12 @@ export const timeSeriesSection: ControlPanelSectionConfig[] = [
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay one or more timeseries from a ' +
'relative time period. Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported.',
),
},
},
],

View File

@@ -271,7 +271,10 @@ function sortHierarchicalObject(
return result;
}
function convertToNumberIfNumeric(value: string): string | number {
function convertToNumberIfNumeric(value: unknown): unknown {
if (typeof value !== 'string') {
return value;
}
const n = Number(value);
return value.trim() !== '' && !Number.isNaN(n) ? n : value;
}

View File

@@ -1052,7 +1052,7 @@ test('renderTableRow uses active header surface for adaptive contrast', () => {
});
function makeColPivotSettings(
value: string,
value: unknown,
): Parameters<TableRenderer['renderColHeaderRow']>[2] {
return {
rowAttrs: [],
@@ -1146,3 +1146,45 @@ test.each([
expect(formatter).toHaveBeenCalledWith(expected);
},
);
test('col header date formatter does not throw when value is a Date object', () => {
const dateValue = new Date(1700000000000);
const formatter = jest.fn().mockReturnValue('formatted');
tableRenderer = new TableRenderer({
...mockProps,
cols: ['event_time'],
tableOptions: {
...mockProps.tableOptions,
dateFormatters: { event_time: formatter },
},
});
expect(() =>
tableRenderer.renderColHeaderRow(
'event_time',
0,
makeColPivotSettings(dateValue),
),
).not.toThrow();
expect(formatter).toHaveBeenCalledWith(dateValue);
});
test('row header date formatter does not throw when value is a Date object', () => {
const dateValue = new Date(1700000000000);
const formatter = jest.fn().mockReturnValue('formatted');
tableRenderer = new TableRenderer({
...mockProps,
rows: ['event_time'],
tableOptions: {
...mockProps.tableOptions,
dateFormatters: { event_time: formatter },
},
});
expect(() =>
tableRenderer.renderTableRow(
[dateValue as unknown as string],
0,
makeRowPivotSettings(),
),
).not.toThrow();
expect(formatter).toHaveBeenCalledWith(dateValue);
});

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
/// <reference types="@emotion/jest" />
import fetchMock from 'fetch-mock';
import {
fireEvent,
@@ -42,6 +43,7 @@ import {
import { storeWithState } from 'spec/fixtures/mockStore';
import mockState from 'spec/fixtures/mockState';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import * as urlUtils from 'src/utils/urlUtils';
import * as useNativeFiltersModule from './state';
fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {});
@@ -456,6 +458,116 @@ describe('DashboardBuilder', () => {
expect(queryByTestId('dashboard-filters-panel')).not.toBeInTheDocument();
});
test('should hide filter panel in standalone=3 report mode', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should show filter panel in standalone=3 when show_filters=true', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return true;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).not.toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should hide filter panel in standalone=3 when show_filters=false', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return false;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId } = setup();
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).toHaveStyleRule('display', 'none');
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
test('should show filters but hide tab navigation in report mode with show_filters=true on tabbed dashboard', () => {
const urlParamSpy = (
jest.spyOn(urlUtils, 'getUrlParam') as jest.SpyInstance
).mockImplementation((param: { name: string }) => {
if (param.name === 'standalone') return 3;
if (param.name === 'show_filters') return true;
return null;
});
const nativeFiltersSpy = jest
.spyOn(useNativeFiltersModule, 'useNativeFilters')
.mockReturnValue({
showDashboard: true,
missingInitialFilters: [],
dashboardFiltersOpen: true,
toggleDashboardFiltersOpen: jest.fn(),
nativeFiltersEnabled: true,
});
try {
const { getByTestId, queryByRole } = setup({
dashboardLayout: undoableDashboardLayoutWithTabs,
});
const filterPanel = getByTestId('dashboard-filters-panel');
expect(filterPanel).not.toHaveStyleRule('display', 'none');
expect(queryByRole('tablist')).not.toBeInTheDocument();
} finally {
urlParamSpy.mockRestore();
nativeFiltersSpy.mockRestore();
}
});
});
test('should render ParentSize wrapper with height 100% for tabs', async () => {

View File

@@ -414,6 +414,8 @@ const DashboardBuilder = () => {
: undefined;
const standaloneMode = getUrlParam(URL_PARAMS.standalone);
const isReport = standaloneMode === DashboardStandaloneMode.Report;
const showFiltersUrlParam = getUrlParam(URL_PARAMS.showFilters);
const hideFilterBar = isReport && showFiltersUrlParam !== true;
const hideDashboardHeader =
uiConfig.hideTitle ||
standaloneMode === DashboardStandaloneMode.HideNavAndTitle ||
@@ -511,12 +513,12 @@ const DashboardBuilder = () => {
filterBarOrientation === FilterBarOrientation.Horizontal && (
<FilterBar
orientation={FilterBarOrientation.Horizontal}
hidden={isReport}
hidden={hideFilterBar}
/>
)}
</>
),
[hideDashboardHeader, showFilterBar, filterBarOrientation, isReport],
[hideDashboardHeader, showFilterBar, filterBarOrientation, hideFilterBar],
);
const renderDraggableContent = useCallback(
@@ -578,7 +580,7 @@ const DashboardBuilder = () => {
return (
<FiltersPanel
width={filterBarWidth}
hidden={isReport}
hidden={hideFilterBar}
data-test="dashboard-filters-panel"
>
<StickyPanel ref={containerRef} width={filterBarWidth}>
@@ -603,7 +605,7 @@ const DashboardBuilder = () => {
toggleDashboardFiltersOpen,
filterBarHeight,
filterBarOffset,
isReport,
hideFilterBar,
],
);

View File

@@ -21,6 +21,7 @@ import { t } from '@apache-superset/core/translation';
import { css, useTheme, SupersetTheme } from '@apache-superset/core/theme';
import { FormLabel, InfoTooltip, Tooltip } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
type ValidationError = string;
export type ControlHeaderProps = {
@@ -92,8 +93,15 @@ const ControlHeader: FC<ControlHeaderProps> = ({
>
{description && (
<span>
<Tooltip title={description}>
<Icons.InfoCircleOutlined css={iconStyles} />
<Tooltip
id="description-tooltip"
title={description}
placement="top"
>
<Icons.InfoCircleOutlined
css={iconStyles}
onClick={tooltipOnClick}
/>
</Tooltip>{' '}
</span>
)}

View File

@@ -22,10 +22,6 @@ import {
ControlSubSectionHeader,
} from '@superset-ui/chart-controls';
import { glossary } from '@superset-ui/core';
const TIME_SHIFT_DESCRIPTION = glossary.Advanced_Analytics.Time_Shift.encode();
export const datasourceAndVizType: ControlPanelSectionConfig = {
controlSetRows: [
['datasource'],
@@ -207,7 +203,12 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
description: TIME_SHIFT_DESCRIPTION,
description: t(
'Overlay one or more timeseries from a ' +
'relative time period. Expects relative time deltas ' +
'in natural language (example: 24 hours, 7 days, ' +
'52 weeks, 365 days). Free text is supported.',
),
},
},
{