Compare commits

...

38 Commits

Author SHA1 Message Date
Evan Rusackas
8750154a5e fix(glossary): address review feedback on tooltip glossary integration
- Rename local identity `t()` in superset-ui-core/src/glossary/glossary.ts
  to `noTranslate()` so it no longer visually shadows the imported i18n
  `t` used elsewhere in the package (resolves Copilot review thread).
- Drop the no-op `t(SENTINEL_DESCRIPTION)` wrappers in 8 chart-control
  files: the wrapped values are `[GLOSSARY]|topic|title` sentinels, not
  translatable strings, so wrapping them in `t()` did nothing at runtime
  and polluted pybabel extraction. Real translation already happens at
  resolution time inside `resolveGlossaryString` via `term.getShort(t)`.
- Memoize `scrollToRow` with `useCallback` in docs/docs/glossary.mdx and
  add it to the `useEffect` deps array (resolves Copilot review thread);
  also wrap the `decodeURIComponent(window.location.hash)` call in
  try/catch so malformed percent-encoding can't break the page render.
- Drop the bare `export default {}` from
  superset-ui-core/src/glossary/index.ts; it shadowed the named
  `glossary` export so default-imports silently received `{}`. All
  in-repo consumers use named imports, so this is safe.

Out of scope (left for the original author):
- Unit tests for resolveGlossaryString, Tooltip branching, and the
  GlossaryTerm.encode roundtrip.
- Making GLOSSARY_BASE_URL configurable for downstream deploys.
- Simplifying the parallel glossaryDefinition / GlossaryTerm /
  GlossaryMap object models.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-27 01:48:09 -07:00
Ashvin-Sr
5c2a001270 Merge branch 'master' into feat/tooltip-docs-integration 2025-12-09 16:38:55 -05:00
CharelSuarez
15617a788c fix: (lint) fix linter errors for our modified files 2025-12-09 15:43:32 -05:00
CharelSuarez
dc53a9038a fix: (lint) use template literal isntead of string concatenation 2025-12-09 12:34:29 -05:00
Ashvin-Sr
ec57679ac6 fix: trimmed leading/trailling whitespaces for encoded glossary string 2025-12-05 16:24:45 -05:00
Ashvin-Sr
9c72e3bf0e chore: add safeguard if extended is null to glossaryUtils 2025-12-05 16:16:55 -05:00
Ashvin-Sr
389a5d95dd chore: fix sidebar to have single quotes instead 2025-12-05 16:05:09 -05:00
Ashvin-Sr
a8a5c36142 fix: update glossary.mdx to use encoded key when searching the DOM
fix: remove stale entries in the DOM if they become null
2025-12-05 16:03:21 -05:00
Ashvin-Sr
d55789c6eb fix: update url to be superset.apache.org
extract functionallity from glossaryUtils into tooltipUtils
2025-12-05 15:40:51 -05:00
Abejithp
9c372e7a16 docs: update comment to show correct encoding format 2025-12-04 15:51:31 -05:00
Abejithp
2a80689763 fix: remove circular dependency by using a local type definition 2025-12-04 15:48:16 -05:00
Abejithp
ac279ba873 fix: clean up tooltip component and address logic improvements 2025-12-04 15:23:02 -05:00
Abejithp
47cacae15c chore: updated the version for antd and caniuse-lite 2025-12-04 15:03:01 -05:00
Abejithp
d56dbc28aa fixed topic name and terms for advanced analytics 2025-11-30 17:32:43 -05:00
Abejithp
d1eb412d06 added min width styling 2025-11-30 14:37:16 -05:00
Abejithp
3fc0dec9d5 added parsing for topic name 2025-11-30 14:24:38 -05:00
Abejithp
f5283e62d4 refactored encoding to enable translations 2025-11-30 14:12:51 -05:00
Ashvin-Sr
0d5017d9ba Updated Glossary so it scrolls based on Query
Added More Tooltips
Cleaned the Glossary
2025-11-30 14:01:16 -05:00
Ashvin-Sr
4683485e2d Updated Row_Limit title to match the one in glossary 2025-11-28 17:54:30 -05:00
Abejithp
102f543792 Added parsing for terms on the glossary documentation page 2025-11-28 13:23:08 -05:00
Abejithp
d77b76e9a5 added more terms to glossary and implemented the tooltips 2025-11-28 13:03:41 -05:00
Abejithp
2891c75cc3 added description encoding to simplfy Tooltip implementation 2025-11-30 17:07:11 -05:00
Abejithp
3565111528 added description encoding to simplfy Tooltip implementation 2025-11-27 22:54:18 -05:00
CharelSuarez
ff999e0fdd Change glossary to be defined statically. 2025-11-27 23:22:51 -05:00
Abejithp
6cbce682bc removed glossary component and extended Tooltip component 2025-11-27 21:28:51 -05:00
Abejithp
3dccaf107d -updated base path for url
-fixed id in glossary table
2025-11-27 21:08:58 -05:00
CharelSuarez
80244a0802 Fix stuff after merge, use new superset frontend core ui glossary instead of other classes. Update docs to use 1 table for each topic. 2025-11-27 20:49:34 -05:00
CharelSuarez
64770df289 Fixed resolving import error on the glossary mdx page. 2025-11-27 20:15:18 -05:00
CharelSuarez
5756e74cf8 Move glossary to src instead of translations, keep trying to fix docs import 2025-11-27 20:14:01 -05:00
CharelSuarez
93eafd7db7 Update glossary to use common library. 2025-11-27 20:13:33 -05:00
CharelSuarez
db01f04bac Try to make a common glossary typescript file to be used in the frontend module. 2025-11-27 20:12:01 -05:00
Abejithp
b7e87ab1aa -moved tooltip component to superset-ui
-implemented an example use case
2025-11-27 16:50:24 -05:00
Abejithp
e3fd203183 added GlossaryTooltip component to be used within the superset app 2025-11-24 22:35:15 -05:00
Ashvin-Sr
95ad5e1be9 Used antd to display the table 2025-11-24 19:10:48 -05:00
CharelSuarez
4a65f34402 Change glossary to typescript instead of json. 2025-11-24 17:06:22 -05:00
Abejithp
a411dbba9a - Removed glossary from version-6.0.0
- Updated styling for glossary entries
2025-11-24 16:30:40 -05:00
Ashvin-Sr
6caa5a89a4 Added a glossary json file to store display the key value tooltips 2025-11-23 22:53:08 -05:00
Ashvin-Sr
0a506ba76a Added glossary page and updated sidebars to include glossary section. 2025-11-23 15:02:18 -05:00
20 changed files with 658 additions and 100 deletions

122
docs/docs/glossary.mdx Normal file
View File

@@ -0,0 +1,122 @@
---
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

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

View File

@@ -41,10 +41,6 @@ export default function webpackExtendPlugin(): Plugin<void> {
...config.resolve.alias,
// Allow importing from superset-frontend
src: path.resolve(__dirname, '../../superset-frontend/src'),
// '@superset-ui/core': path.resolve(
// __dirname,
// '../../superset-frontend/packages/superset-ui-core',
// ),
// Add aliases for our components to make imports easier
'@docs/components': path.resolve(__dirname, '../src/components'),
'@superset/components': path.resolve(

View File

@@ -3984,9 +3984,9 @@
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/lodash@^4.14.146":
version "4.17.20"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
version "4.17.21"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.21.tgz#b806831543d696b14f8112db600ea9d3a1df6ea4"
integrity sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==
"@types/mdast@^3.0.0":
version "3.0.15"
@@ -11158,9 +11158,9 @@ pretty-error@^4.0.0:
renderkid "^3.0.0"
pretty-ms@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0"
integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==
version "9.3.0"
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.3.0.tgz#dd2524fcb3c326b4931b2272dfd1e1a8ed9a9f5a"
integrity sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==
dependencies:
parse-ms "^4.0.0"

View File

@@ -22,6 +22,10 @@ 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',
@@ -122,12 +126,7 @@ export const advancedAnalyticsControls: ControlPanelSectionConfig = {
['156 weeks ago', t('156 weeks ago')],
['3 years ago', t('3 years ago')],
],
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
],

View File

@@ -24,6 +24,10 @@ 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')],
@@ -81,16 +85,7 @@ export const timeComparisonControls: ({
placeholder: t('Select or type a custom value...'),
label: t('Time shift'),
choices: showFullChoices ? fullChoices : reducedChoices,
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
],

View File

@@ -38,6 +38,13 @@ 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;
@@ -77,11 +84,7 @@ export const dndGroupByControl: SharedControlConfig<
clearable: true,
default: [],
includeTime: false,
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.',
),
description: DIMENSION_DESCRIPTION,
optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
valueKey: 'column_name',
@@ -179,11 +182,7 @@ export const dndAdhocMetricsControl: SharedControlConfig<
datasource,
datasourceType: datasource?.type,
}),
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.',
),
description: METRIC_DESCRIPTION,
};
export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
@@ -223,11 +222,7 @@ export const dndSortByControl: SharedControlConfig<
type: 'DndMetricSelect',
label: t('Sort query by'),
default: null,
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).',
),
description: SORT_DESCRIPTION,
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),

View File

@@ -86,6 +86,10 @@ 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();
@@ -235,9 +239,7 @@ const row_limit: SharedControlConfig<'SelectControl'> = {
],
default: 10000,
choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
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.',
),
description: ROW_LIMIT_DESCRIPTION,
};
const order_desc: SharedControlConfig<'CheckboxControl'> = {
@@ -250,7 +252,7 @@ const order_desc: SharedControlConfig<'CheckboxControl'> = {
visibility: ({ controls }) =>
Boolean(
controls?.timeseries_limit_metric.value &&
!isEmpty(controls?.timeseries_limit_metric.value),
!isEmpty(controls?.timeseries_limit_metric.value),
),
};
@@ -262,12 +264,7 @@ const limit: SharedControlConfig<'SelectControl'> = {
validators: [legacyValidateInteger],
choices: formatSelectOptions(SERIES_LIMITS),
clearable: true,
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.',
),
description: SERIES_DESCRIPTION,
};
const series_limit: SharedControlConfig<'SelectControl'> = {
@@ -277,12 +274,7 @@ const series_limit: SharedControlConfig<'SelectControl'> = {
placeholder: t('None'),
validators: [legacyValidateInteger],
choices: formatSelectOptions(SERIES_LIMITS),
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.',
),
description: SERIES_DESCRIPTION,
};
const group_others_when_limit_reached: SharedControlConfig<'CheckboxControl'> =

View File

@@ -16,17 +16,70 @@
* 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

@@ -0,0 +1,121 @@
/**
* 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

@@ -0,0 +1,154 @@
/**
* 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

@@ -0,0 +1,63 @@
/**
* 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

@@ -0,0 +1,26 @@
/*
* 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

@@ -0,0 +1,50 @@
/**
* 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

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

View File

@@ -27,6 +27,9 @@ 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: [
@@ -320,12 +323,7 @@ const config: ControlPanelConfig = {
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
{

View File

@@ -26,6 +26,9 @@ 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: [
@@ -204,12 +207,7 @@ const config: ControlPanelConfig = {
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
{

View File

@@ -28,6 +28,10 @@ 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:
@@ -472,12 +476,7 @@ export const timeSeriesSection: ControlPanelSectionConfig[] = [
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
],

View File

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

View File

@@ -22,6 +22,10 @@ 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'],
@@ -203,12 +207,7 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [
['156 weeks', t('156 weeks')],
['3 years', t('3 years')],
],
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.',
),
description: TIME_SHIFT_DESCRIPTION,
},
},
{