Compare commits

...

6 Commits

Author SHA1 Message Date
Elizabeth Thompson
98212189b8 fix(docker): bind webpack dev server to all interfaces
Change WEBPACK_DEVSERVER_HOST from 127.0.0.1 to 0.0.0.0 to make the
frontend accessible from outside the Docker container. This fixes an
issue where the frontend was only accessible from inside the container.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 17:03:49 -07:00
SBIN2010
1e4bc6ee78 fix: bug in tooltip timeseries chart in calculated total with annotation layer (#35179) 2025-09-19 10:30:42 -07:00
Pat Buxton
db178cf527 fix: Bump pandas to 2.1.4 for python 3.12 (#34999) 2025-09-19 09:18:00 -07:00
Alexandru Soare
5901320933 feat(database): Adding per-user caching option in Security tab (#34842) 2025-09-19 19:15:31 +03:00
SBIN2010
23bb4f88c0 fix(Funnel): onInit overridden row_limit to default value on save chart (#35076) 2025-09-19 09:13:45 -07:00
Levis Mbote
4130b92966 fix(gantt-chart): fix Y-axis label visibility in dark theme (#35189) 2025-09-19 12:33:53 +03:00
14 changed files with 113 additions and 61 deletions

View File

@@ -163,7 +163,7 @@ services:
# configuring the dev-server to use the host.docker.internal to connect to the backend # configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset-light:8088" superset: "http://superset-light:8088"
# Webpack dev server configuration # Webpack dev server configuration
WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-127.0.0.1}" WEBPACK_DEVSERVER_HOST: "${WEBPACK_DEVSERVER_HOST:-0.0.0.0}"
WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}" WEBPACK_DEVSERVER_PORT: "${WEBPACK_DEVSERVER_PORT:-9000}"
ports: ports:
- "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces - "${NODE_PORT:-9001}:9000" # Parameterized port, accessible on all interfaces

View File

@@ -76,7 +76,7 @@ dependencies = [
"packaging", "packaging",
# -------------------------- # --------------------------
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed) # pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
"pandas[excel]>=2.0.3, <2.1", "pandas[excel]>=2.0.3, <2.2",
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended "bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
# -------------------------- # --------------------------
"parsedatetime", "parsedatetime",

View File

@@ -160,6 +160,7 @@ greenlet==3.1.1
# via # via
# apache-superset (pyproject.toml) # apache-superset (pyproject.toml)
# shillelagh # shillelagh
# sqlalchemy
gunicorn==23.0.0 gunicorn==23.0.0
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
h11==0.16.0 h11==0.16.0
@@ -266,7 +267,7 @@ packaging==25.0
# limits # limits
# marshmallow # marshmallow
# shillelagh # shillelagh
pandas==2.0.3 pandas==2.1.4
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
paramiko==3.5.1 paramiko==3.5.1
# via # via

View File

@@ -331,6 +331,7 @@ greenlet==3.1.1
# apache-superset # apache-superset
# gevent # gevent
# shillelagh # shillelagh
# sqlalchemy
grpcio==1.71.0 grpcio==1.71.0
# via # via
# apache-superset # apache-superset
@@ -536,7 +537,7 @@ packaging==25.0
# pytest # pytest
# shillelagh # shillelagh
# sqlalchemy-bigquery # sqlalchemy-bigquery
pandas==2.0.3 pandas==2.1.4
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset

View File

@@ -19,7 +19,6 @@
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { import {
ControlPanelConfig, ControlPanelConfig,
ControlStateMapping,
ControlSubSectionHeader, ControlSubSectionHeader,
D3_FORMAT_DOCS, D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS, D3_FORMAT_OPTIONS,
@@ -197,15 +196,6 @@ const config: ControlPanelConfig = {
], ],
}, },
], ],
onInit(state: ControlStateMapping) {
return {
...state,
row_limit: {
...state.row_limit,
value: state.row_limit.default,
},
};
},
formDataOverrides: formData => ({ formDataOverrides: formData => ({
...formData, ...formData,
metric: getStandardizedControls().shiftMetric(), metric: getStandardizedControls().shiftMetric(),

View File

@@ -325,6 +325,7 @@ export default function transformProps(chartProps: EchartsGanttChartProps) {
show: true, show: true,
position: 'start', position: 'start',
formatter: '{b}', formatter: '{b}',
color: theme.colorText,
}, },
data: categoryLines, data: categoryLines,
}, },

View File

@@ -47,7 +47,10 @@ import {
isDerivedSeries, isDerivedSeries,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
import type { EChartsCoreOption } from 'echarts/core'; import type { EChartsCoreOption } from 'echarts/core';
import type { LineStyleOption } from 'echarts/types/src/util/types'; import type {
LineStyleOption,
CallbackDataParams,
} from 'echarts/types/src/util/types';
import type { SeriesOption } from 'echarts'; import type { SeriesOption } from 'echarts';
import { import {
EchartsTimeseriesChartProps, EchartsTimeseriesChartProps,
@@ -575,16 +578,31 @@ export default function transformProps(
const xValue: number = richTooltip const xValue: number = richTooltip
? params[0].value[xIndex] ? params[0].value[xIndex]
: params.value[xIndex]; : params.value[xIndex];
const forecastValue: any[] = richTooltip ? params : [params]; const forecastValue: CallbackDataParams[] = richTooltip
? params
: [params];
const sortedKeys = extractTooltipKeys( const sortedKeys = extractTooltipKeys(
forecastValue, forecastValue,
yIndex, yIndex,
richTooltip, richTooltip,
tooltipSortByMetric, tooltipSortByMetric,
); );
const filteredForecastValue = forecastValue.filter(
(item: CallbackDataParams) =>
!annotationLayers.some(
(annotation: AnnotationLayer) =>
item.seriesName === annotation.name,
),
);
const forecastValues: Record<string, ForecastValue> = const forecastValues: Record<string, ForecastValue> =
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal); extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
const filteredForecastValues: Record<string, ForecastValue> =
extractForecastValuesFromTooltipParams(
filteredForecastValue,
isHorizontal,
);
const isForecast = Object.values(forecastValues).some( const isForecast = Object.values(forecastValues).some(
value => value =>
value.forecastTrend || value.forecastLower || value.forecastUpper, value.forecastTrend || value.forecastLower || value.forecastUpper,
@@ -595,7 +613,7 @@ export default function transformProps(
: (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter); : (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter);
const rows: string[][] = []; const rows: string[][] = [];
const total = Object.values(forecastValues).reduce( const total = Object.values(filteredForecastValues).reduce(
(acc, value) => (acc, value) =>
value.observation !== undefined ? acc + value.observation : acc, value.observation !== undefined ? acc + value.observation : acc,
0, 0,
@@ -617,7 +635,16 @@ export default function transformProps(
seriesName: key, seriesName: key,
formatter, formatter,
}); });
if (showPercentage && value.observation !== undefined) {
const annotationRow = annotationLayers.some(
item => item.name === key,
);
if (
showPercentage &&
value.observation !== undefined &&
!annotationRow
) {
row.push( row.push(
percentFormatter.format(value.observation / (total || 1)), percentFormatter.format(value.observation / (total || 1)),
); );

View File

@@ -257,6 +257,7 @@ describe('Gantt transformProps', () => {
show: true, show: true,
position: 'start', position: 'start',
formatter: '{b}', formatter: '{b}',
color: 'rgba(0,0,0,0.88)',
}, },
lineStyle: expect.objectContaining({ lineStyle: expect.objectContaining({
color: '#00000000', color: '#00000000',

View File

@@ -460,48 +460,26 @@ const ExtraOptions = ({
), ),
children: ( children: (
<> <>
<StyledInputContainer> <StyledInputContainer
<div className="control-label">{t('Secure extra')}</div> css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
>
<div className="input-container"> <div className="input-container">
<StyledJsonEditor <Checkbox
name="masked_encrypted_extra" id="per_user_caching"
value={db?.masked_encrypted_extra || ''} name="per_user_caching"
placeholder={t('Secure extra')} indeterminate={false}
onChange={(json: string) => checked={!!extraJson?.per_user_caching}
onEditorChange({ json, name: 'masked_encrypted_extra' }) onChange={onExtraInputChange}
} >
width="100%" {t('Per user caching')}
height="160px" </Checkbox>
annotations={secureExtraAnnotations} <InfoTooltip
/> tooltip={t(
</div> 'Cache data separately for each user based on their data access roles and permissions. ' +
<div className="helper"> 'When disabled, a single cache will be used for all users.',
<div>
{t(
'JSON string containing additional connection configuration. ' +
'This is used to provide connection information for systems ' +
'like Hive, Presto and BigQuery which do not conform to the ' +
'username:password syntax normally used by SQLAlchemy.',
)} )}
</div>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Root certificate')}</div>
<div className="input-container">
<Input.TextArea
name="server_cert"
value={db?.server_cert || ''}
placeholder={t('Enter CA_BUNDLE')}
onChange={onTextChange}
/> />
</div> </div>
<div className="helper">
{t(
'Optional CA_BUNDLE contents to validate HTTPS requests. Only ' +
'available on certain database engines.',
)}
</div>
</StyledInputContainer> </StyledInputContainer>
<StyledInputContainer <StyledInputContainer
css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}} css={!isFileUploadSupportedByEngine ? no_margin_bottom : {}}
@@ -569,6 +547,49 @@ const ExtraOptions = ({
</div> </div>
</StyledInputContainer> </StyledInputContainer>
)} )}
<StyledInputContainer>
<div className="control-label">{t('Secure extra')}</div>
<div className="input-container">
<StyledJsonEditor
name="masked_encrypted_extra"
value={db?.masked_encrypted_extra || ''}
placeholder={t('Secure extra')}
onChange={(json: string) =>
onEditorChange({ json, name: 'masked_encrypted_extra' })
}
width="100%"
height="160px"
annotations={secureExtraAnnotations}
/>
</div>
<div className="helper">
<div>
{t(
'JSON string containing additional connection configuration. ' +
'This is used to provide connection information for systems ' +
'like Hive, Presto and BigQuery which do not conform to the ' +
'username:password syntax normally used by SQLAlchemy.',
)}
</div>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Root certificate')}</div>
<div className="input-container">
<Input.TextArea
name="server_cert"
value={db?.server_cert || ''}
placeholder={t('Enter CA_BUNDLE')}
onChange={onTextChange}
/>
</div>
<div className="helper">
{t(
'Optional CA_BUNDLE contents to validate HTTPS requests. Only ' +
'available on certain database engines.',
)}
</div>
</StyledInputContainer>
</> </>
), ),
}, },

View File

@@ -246,6 +246,7 @@ export interface ExtraJson {
disable_data_preview?: boolean; // in SQL Lab disable_data_preview?: boolean; // in SQL Lab
disable_drill_to_detail?: boolean; disable_drill_to_detail?: boolean;
allow_multi_catalog?: boolean; allow_multi_catalog?: boolean;
per_user_caching?: boolean; // in Security
engine_params?: { engine_params?: {
catalog?: Record<string, string>; catalog?: Record<string, string>;
connect_args?: { connect_args?: {

View File

@@ -72,7 +72,7 @@ class ExcelReader(BaseDataReader):
"na_values": self._options.get("null_values") "na_values": self._options.get("null_values")
if self._options.get("null_values") # None if an empty list if self._options.get("null_values") # None if an empty list
else None, else None,
"parse_dates": self._options.get("column_dates"), "parse_dates": self._options.get("column_dates") or False,
"skiprows": self._options.get("skip_rows", 0), "skiprows": self._options.get("skip_rows", 0),
"sheet_name": self._options.get("sheet_name", 0), "sheet_name": self._options.get("sheet_name", 0),
"nrows": self._options.get("rows_to_read"), "nrows": self._options.get("rows_to_read"),

View File

@@ -454,13 +454,19 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
cache_dict["annotation_layers"] = annotation_layers cache_dict["annotation_layers"] = annotation_layers
# Add an impersonation key to cache if impersonation is enabled on the db # Add an impersonation key to cache if impersonation is enabled on the db
# or if the CACHE_QUERY_BY_USER flag is on # or if the CACHE_QUERY_BY_USER flag is on or per_user_caching is enabled on
# the database
try: try:
database = self.datasource.database # type: ignore database = self.datasource.database # type: ignore
extra = json.loads(database.extra or "{}")
if ( if (
(
feature_flag_manager.is_feature_enabled("CACHE_IMPERSONATION") feature_flag_manager.is_feature_enabled("CACHE_IMPERSONATION")
and database.impersonate_user and database.impersonate_user
) or feature_flag_manager.is_feature_enabled("CACHE_QUERY_BY_USER"): )
or feature_flag_manager.is_feature_enabled("CACHE_QUERY_BY_USER")
or extra.get("per_user_caching", False)
):
if key := database.db_engine_spec.get_impersonation_key( if key := database.db_engine_spec.get_impersonation_key(
getattr(g, "user", None) getattr(g, "user", None)
): ):

View File

@@ -831,6 +831,7 @@ class ImportV1DatabaseExtraSchema(Schema):
disable_data_preview = fields.Boolean(required=False) disable_data_preview = fields.Boolean(required=False)
disable_drill_to_detail = fields.Boolean(required=False) disable_drill_to_detail = fields.Boolean(required=False)
allow_multi_catalog = fields.Boolean(required=False) allow_multi_catalog = fields.Boolean(required=False)
per_user_caching = fields.Boolean(required=False)
version = fields.String(required=False, allow_none=True) version = fields.String(required=False, allow_none=True)
schema_options = fields.Dict(keys=fields.Str(), values=fields.Raw()) schema_options = fields.Dict(keys=fields.Str(), values=fields.Raw())

View File

@@ -105,6 +105,8 @@ def data_loader(
pandas_loader_configuration: PandasLoaderConfigurations, pandas_loader_configuration: PandasLoaderConfigurations,
table_to_df_convertor: TableToDfConvertor, table_to_df_convertor: TableToDfConvertor,
) -> DataLoader: ) -> DataLoader:
if example_db_engine.dialect.name == PRESTO:
example_db_engine.dialect.get_view_names = Mock(return_value=[])
return PandasDataLoader( return PandasDataLoader(
example_db_engine, pandas_loader_configuration, table_to_df_convertor example_db_engine, pandas_loader_configuration, table_to_df_convertor
) )