From da27ba59a57fc9d7e43426cff406e1138a2788a6 Mon Sep 17 00:00:00 2001 From: Mehmet Salih Yavuz Date: Tue, 1 Sep 2026 16:16:26 +0300 Subject: [PATCH 01/12] fix(pivot-table): apply "Show values as" percent to exports and reports (#43718) Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> --- superset/charts/client_processing.py | 590 ++++++++++++++++- superset/constants.py | 20 + superset/utils/excel.py | 28 +- superset/utils/pandas_postprocessing/pivot.py | 17 +- .../charts/test_client_processing.py | 609 +++++++++++++++++- 5 files changed, 1218 insertions(+), 46 deletions(-) diff --git a/superset/charts/client_processing.py b/superset/charts/client_processing.py index 3242e0712c00..153b8c3bf1d4 100644 --- a/superset/charts/client_processing.py +++ b/superset/charts/client_processing.py @@ -25,6 +25,7 @@ """ import logging +from collections.abc import Callable from functools import partial from io import BytesIO, StringIO from typing import Any, Optional, TYPE_CHECKING, Union @@ -35,11 +36,14 @@ from flask_babel import gettext as __ from superset.common.chart_data import ChartDataResultFormat +from superset.common.grouping_sets import GROUPING_MARKER_SUFFIX +from superset.constants import SHOW_VALUES_AS_PERCENT_MODES, ShowValuesAs from superset.extensions import event_logger from superset.utils import csv, excel from superset.utils.core import ( extract_dataframe_dtypes, get_column_names, + get_metric_name, get_metric_names, ) from superset.utils.number_format import ( @@ -61,6 +65,14 @@ # ``transformProps`` formatter selection. PERCENT_3_POINT = ",.3%" +# The pivot renderer formats a fraction with ``usFmtPct`` +# (``react-pivottable/utilities.ts``), which is one decimal place -- not the +# Table plugin's three. +PERCENT_1_POINT = ",.1%" + +# The Excel equivalent, applied as a cell format so the value stays numeric. +EXCEL_PERCENT_FORMAT = "0.0%" + def get_column_key(label: tuple[str, ...], metrics: list[str]) -> tuple[Any, ...]: """ @@ -75,6 +87,398 @@ def get_column_key(label: tuple[str, ...], metrics: list[str]) -> tuple[Any, ... return tuple(parts) +# How a metric's rollup total is derived from its cells, mirroring +# `additiveReducerFor` in the pivot plugin's `plugin/utilities.ts`: SUM and +# COUNT add up, MIN takes the lowest, MAX the highest. Everything else (saved +# metrics, adhoc SQL, AVG, ...) is non-additive and has no correct answer at +# this layer, so it falls back to summing the cells. +_ROLLUP_REDUCERS: dict[str, str] = {"MIN": "min", "MAX": "max"} +DEFAULT_ROLLUP_REDUCER = "sum" + + +def split_grouping_sets_levels( + df: pd.DataFrame, +) -> tuple[pd.DataFrame, dict[frozenset[str], pd.DataFrame]]: + """ + Separate a GROUPING SETS result into its leaf frame and rollup levels. + + A pivot chart with non-additive metrics asks for every rollup level in one + frame, tagging each row with a ``GROUPING()`` marker per groupby column + (see ``common/grouping_sets.py``): ``0`` where the column is grouped at that + row's level, ``1`` where it has been rolled up. The rollup rows must not be + pivoted as ordinary rows -- their collapsed dimensions are NULL, so they + would add phantom rows and columns and inflate every denominator. + + They are not discardable either: for a non-additive metric the database + rollup is the only correct total, and re-deriving one from the leaf cells + gives a different number (the mean of means, say, rather than the mean). + The chart divides by these values, so the export has to as well. + + :return: the leaf frame, and each rollup level keyed by its grouped columns + """ + markers = [ + column + for column in df.columns + if isinstance(column, str) and column.endswith(GROUPING_MARKER_SUFFIX) + ] + if not markers: + return df, {} + + grouped_of = {marker: marker[: -len(GROUPING_MARKER_SUFFIX)] for marker in markers} + levels: dict[frozenset[str], pd.DataFrame] = {} + leaf = df + for keys, rows_at_level in df.groupby(markers, sort=False): + # `groupby` yields a scalar key for a single column and a tuple beyond. + marker_values = keys if isinstance(keys, tuple) else (keys,) + grouped = frozenset( + grouped_of[marker] + for marker, rolled_up in zip(markers, marker_values, strict=True) + if not rolled_up + ) + level = rows_at_level.drop(columns=markers).reset_index(drop=True) + levels[grouped] = level + if len(grouped) == len(markers): + leaf = level + return leaf, levels + + +def get_metric_rollup_reducers( + metrics: list[Any], verbose_map: Optional[dict[str, Any]] = None +) -> dict[str, str]: + """Map each metric's label to the reducer that rolls its cells up.""" + reducers: dict[str, str] = {} + for metric in metrics: + reducer = DEFAULT_ROLLUP_REDUCER + if isinstance(metric, dict) and metric.get("expressionType") == "SIMPLE": + reducer = _ROLLUP_REDUCERS.get( + metric.get("aggregate") or "", DEFAULT_ROLLUP_REDUCER + ) + reducers[get_metric_name(metric, verbose_map)] = reducer + return reducers + + +def _collapsed_metric(present: list[Any], metrics: list[str]) -> Any: + """ + The metric a rollup spanning `present` stands for. + + A total that collapses the metric axis is undefined in the renderer, which + resolves it to the last metric pushed into the shared slot (see the + ``metricAxis`` handling in ``react-pivottable/utilities.ts``). Mirror that + by taking the last metric in the configured order, so exported percentages + match the chart rather than summing metrics that share no unit. + """ + distinct = set(present) + if len(distinct) == 1: + return distinct.pop() + for metric in reversed(metrics): + if metric in distinct: + return metric + return None + + +def _broadcast(total: pd.Series, block: pd.DataFrame, axis: int) -> pd.DataFrame: + """Spread a per-row (`axis` 0) or per-column (`axis` 1) total over `block`.""" + if axis == 0: + spread = pd.concat([total] * len(block.columns), axis=1) + spread.columns = block.columns + return spread + return pd.DataFrame( + np.tile(total.reindex(block.columns).to_numpy(), (len(block.index), 1)), + index=block.index, + columns=block.columns, + ) + + +def _metric_of_column(column: Any, metric_level: int) -> Any: + """The metric a pivoted column belongs to.""" + return column[metric_level] if isinstance(column, tuple) else column + + +def _reduce( + data: Union[pd.DataFrame, pd.Series], + reducer: str, + axis: Optional[int] = None, +) -> Any: + """Apply a rollup reducer (``sum``/``min``/``max``), skipping empty cells.""" + method = getattr(data, reducer) + return method(axis=axis) if axis is not None else method() + + +def _rollup_index( + rollup_levels: dict[frozenset[str], pd.DataFrame], +) -> Callable[[list[str]], dict[tuple[str, ...], dict[str, Any]]]: + """ + Index each rollup level by its grouped dimension values, on first use. + + Reshaping a level costs a `fillna` and a `to_dict` over the whole frame, so + it is done once per level rather than once per cell -- the difference + between linear and quadratic on a large pivot. + """ + cache: dict[tuple[str, ...], dict[tuple[str, ...], dict[str, Any]]] = {} + + def keyed(dimensions: list[str]) -> dict[tuple[str, ...], dict[str, Any]]: + cache_key = tuple(dimensions) + if cache_key not in cache: + level = rollup_levels.get(frozenset(dimensions)) + cache[cache_key] = ( + {} + if level is None + else { + tuple(str(record[dimension]) for dimension in dimensions): record + for record in level.fillna("SUPERSET_PANDAS_NAN").to_dict("records") + } + ) + return cache[cache_key] + + return keyed + + +def _rollup_key( + label: Any, depth: int, metric_level: int, is_column: bool +) -> tuple[str, ...]: + """The grouped dimension values a pivoted row or column label carries.""" + parts = list(label) if isinstance(label, tuple) else [label] + if is_column: + # The column label interleaves the metric with the dimension values. + parts = [part for index, part in enumerate(parts) if index != metric_level] + return tuple(str(part) for part in parts[:depth]) + + +def _apply_rollup_totals( # pylint: disable=too-many-arguments,too-many-locals + df: pd.DataFrame, + rows: list[str], + columns: list[str], + metrics: list[str], + rollup_levels: dict[frozenset[str], pd.DataFrame], + metric_level: int, + row_prefix_depth: dict[Any, int], + column_prefix_depth: dict[Any, int], +) -> pd.DataFrame: + """ + Replace inserted totals with the values the database computed. + + A total grouping ``i`` row and ``j`` column dimensions is exactly the rollup + level over ``rows[:i] + columns[:j]``, which ``buildGroupbyCombinations`` + requests whenever the chart displays that total. Reading it keeps the export + equal to the chart for a non-additive metric, where reducing the leaf cells + gives a different number. + + A total the chart did not request keeps its leaf-derived value, so a missing + level degrades to the previous behaviour. A total the database returned as + NULL is kept as NULL, which is not the same thing -- the chart renders that + cell blank. + """ + keyed = _rollup_index(rollup_levels) + metric_names = set(metrics) + + def metric_of(column: Any) -> Any: + name = _metric_of_column(column, metric_level) + return ( + name if name in metric_names else _collapsed_metric(list(metrics), metrics) + ) + + def lookup(row: Any, column: Any) -> tuple[bool, Any]: + row_depth = row_prefix_depth.get(row, len(rows)) + column_depth = column_prefix_depth.get(column, len(columns)) + grouped = rows[:row_depth] + columns[:column_depth] + key = _rollup_key(row, row_depth, metric_level, is_column=False) + _rollup_key( + column, column_depth, metric_level, is_column=True + ) + record = keyed(grouped).get(key) + if record is None: + return False, None + return True, record.get(metric_of(column)) + + # Index positionally: a tuple label on a MultiIndex is ambiguous to `.loc`. + for column_position, column in enumerate(df.columns): + for row_position, row in enumerate(df.index): + if row not in row_prefix_depth and column not in column_prefix_depth: + continue # a leaf cell, already carrying its own value + found, value = lookup(row, column) + if found: + df.iloc[row_position, column_position] = value + return df + + +def _rollup_denominators( # pylint: disable=too-many-arguments,too-many-locals + df: pd.DataFrame, + mode: str, + rows: list[str], + columns: list[str], + metrics: list[str], + rollup_levels: dict[frozenset[str], pd.DataFrame], + metric_level: int, + row_prefix_depth: dict[Any, int], + column_prefix_depth: dict[Any, int], + metrics_on_rows: bool, +) -> tuple[pd.DataFrame, pd.DataFrame]: + """ + Each cell's denominator, taken from the database-computed rollup levels. + + A percent mode makes the chart request the level its denominator needs: for + "% of row" the cell's own row with the columns collapsed, for "% of column" + the reverse, for "% of total" both (see ``buildGroupbyCombinations``). A + subtotal divides by its own prefix, not by the grand total -- an "EU" + subtotal row divides by the ``{region}`` rollup. + + The metrics layout decides which frame axis carries the displayed rows, so + "% of row" groups the column dimensions when metrics sit on rows. + + :return: the denominators, and a mask of the cells the database resolved. + A resolved cell holding NULL stays NULL, where an unresolved one lets + the caller fall back to a leaf-derived total. + """ + keyed = _rollup_index(rollup_levels) + metric_names = set(metrics) + + def metric_of(column: Any) -> Any: + name = _metric_of_column(column, metric_level) + return ( + name if name in metric_names else _collapsed_metric(list(metrics), metrics) + ) + + def denominator(row: Any, column: Any) -> tuple[bool, Any]: + if mode == ShowValuesAs.PERCENT_OF_TOTAL: + grouped: list[str] = [] + key: tuple[str, ...] = () + else: + # The displayed row axis is the frame index, unless the metrics + # layout moved it to the columns. + along_index = (mode == ShowValuesAs.PERCENT_OF_ROW) != metrics_on_rows + if along_index: + depth = row_prefix_depth.get(row, len(rows)) + grouped = rows[:depth] + key = _rollup_key(row, depth, metric_level, is_column=False) + else: + depth = column_prefix_depth.get(column, len(columns)) + grouped = columns[:depth] + key = _rollup_key(column, depth, metric_level, is_column=True) + record = keyed(grouped).get(key) + if record is None: + return False, None + return True, record.get(metric_of(column)) + + resolved = [[denominator(row, column) for column in df.columns] for row in df.index] + values = pd.DataFrame( + [[value for _, value in row] for row in resolved], + index=df.index, + columns=df.columns, + ) + found = pd.DataFrame( + [[hit for hit, _ in row] for row in resolved], + index=df.index, + columns=df.columns, + ) + return values.apply(pd.to_numeric, errors="coerce").astype(float), found + + +def _apply_show_values_as( # pylint: disable=too-many-arguments + df: pd.DataFrame, + mode: str, + axis: dict[str, int], + metrics: list[str], + combine_metrics: bool, + inserted_rows: list[Any], + inserted_columns: list[Any], + reducers: dict[str, str], + denominators: Optional[tuple[pd.DataFrame, pd.DataFrame]] = None, +) -> pd.DataFrame: + """ + Express each cell as a fraction of its row, column, or grand total. + + Mirrors the client's ``fractionOf`` aggregator in + ``plugin-chart-pivot-table/src/react-pivottable/utilities.ts``. Two details + it inherits from there: + + - Denominators are summed over leaf cells only. Totals and subtotals + inserted into the frame are numerators like any other cell -- a "% of + row" grand total row reads ``column total / grand total``, not the sum of + the fractions above it. + - A total is rolled up within a single metric, so a cell is never divided by + a total that mixes in another metric, and each metric uses its own + reducer -- a MIN/MAX metric divides by the row's minimum/maximum rather + than its sum. A total that collapses the metric axis resolves to a single + metric the way the renderer does; see ``_collapsed_metric``. + + A zero denominator yields NaN (blank) rather than infinity, matching + ``pandas_postprocessing.pivot``'s ``show_values_as``. + """ + numeric = df.apply(pd.to_numeric, errors="coerce").astype(float) + is_multi_index = isinstance(df.columns, pd.MultiIndex) + # `combine_metrics` has already moved the metric to the lowest column level. + metric_level = df.columns.nlevels - 1 if combine_metrics and is_multi_index else 0 + metric_names = set(metrics) + metric_of_column = [ + key if key in metric_names else None + for key in df.columns.get_level_values(metric_level) + ] + leaf_rows = ~df.index.isin(inserted_rows) + leaf_columns = ~df.columns.isin(inserted_columns) + + derived = pd.DataFrame(np.nan, index=numeric.index, columns=numeric.columns) + for metric in dict.fromkeys(metric_of_column): + selection = np.array([column == metric for column in metric_of_column]) + denominator_selection = ( + selection if metric is not None else np.ones(len(selection), dtype=bool) + ) + # Derive the reducer from the columns forming the denominator, not from + # the numerator's own label: a total column carries a total label, but + # must still divide by a rollup of the metric it totals. + denominator_metric = _collapsed_metric( + [ + column_metric + for column_metric, keep in zip( + metric_of_column, denominator_selection, strict=True + ) + if keep and column_metric is not None + ], + metrics, + ) + reducer = reducers.get(str(denominator_metric), DEFAULT_ROLLUP_REDUCER) + if denominator_metric is not None: + denominator_selection = denominator_selection & np.array( + [column == denominator_metric for column in metric_of_column] + ) + block = numeric.loc[:, selection] + if mode == ShowValuesAs.PERCENT_OF_TOTAL: + leaf = numeric.loc[leaf_rows, leaf_columns & denominator_selection] + # Reduce through pandas, not numpy: a sparse pivot leaves NaN in + # cells whose group had no rows, and numpy would propagate that to + # the grand total, blanking every cell. + grand_total = _reduce(_reduce(leaf, reducer, axis=0), reducer) + group_denominator = pd.DataFrame( + np.nan if pd.isna(grand_total) else grand_total, + index=block.index, + columns=block.columns, + ) + else: + summed, divided = ( + (axis["rows"], axis["columns"]) + if mode == ShowValuesAs.PERCENT_OF_COLUMN + else (axis["columns"], axis["rows"]) + ) + # The metric lives on the column axis, so only a rollup taken along + # that axis has to stay within one metric. + leaf = ( + numeric.loc[:, leaf_columns & denominator_selection] + if summed == 1 + else numeric.loc[leaf_rows, :] + ) + total = _reduce(leaf, reducer, axis=summed) + group_denominator = _broadcast(total, block, divided) + derived.loc[:, selection] = group_denominator + + denominator = derived + if denominators is not None: + # Database-computed rollups win wherever the chart requested the level. + # Mask on whether the level resolved, not on whether the value is null: + # a rollup the database returned as NULL leaves the cell blank, as the + # chart does, while an unrequested level falls back to the leaf total. + values, found = denominators + denominator = derived.mask(found, values) + return numeric / denominator.replace(0, np.nan) + + def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-statements, too-many-branches # noqa: C901 df: pd.DataFrame, rows: list[str], @@ -87,8 +491,31 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s show_columns_total: bool = False, apply_metrics_on_rows: bool = False, metric_name_aggfunc: Optional[str] = None, + show_values_as: Optional[str] = None, + metric_rollup_reducers: Optional[dict[str, str]] = None, + rollup_levels: Optional[dict[frozenset[str], pd.DataFrame]] = None, ) -> pd.DataFrame: + percent_mode = ( + show_values_as if show_values_as in SHOW_VALUES_AS_PERCENT_MODES else None + ) + reducers = metric_rollup_reducers or {} + if percent_mode: + # The chart ignores `aggregateFunction` post-SIP-216: cells arrive + # pre-aggregated from the database and totals are per-metric rollups of + # them. Match that here so the totals and the percent denominators + # cannot disagree -- otherwise a total stops dividing by itself and the + # Total row/column reads something other than 100%. + aggfunc = "Sum" metric_name = __("Total (%(aggfunc)s)", aggfunc=metric_name_aggfunc or aggfunc) + # Labels of the total/subtotal rows and columns inserted below, so the + # `showValuesAs` denominators can be summed over leaf cells only. + inserted_rows: list[Any] = [] + inserted_columns: list[Any] = [] + # How many dimensions of its own axis each inserted total still groups; 0 + # collapses the axis entirely. Together with the other axis they name the + # rollup level holding that total's database-computed value. + row_prefix_depth: dict[Any, int] = {} + column_prefix_depth: dict[Any, int] = {} if transpose_pivot: rows, columns = columns, rows @@ -98,6 +525,11 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s # returning it if apply_metrics_on_rows: rows, columns = columns, rows + # The frame is transposed on the way out, which flips the axis each + # total was inserted on. Swap the toggles too, so `rowTotals` still + # means the right-hand Total column of the rendered table, whether or + # not there are column dimensions to group by. + show_rows_total, show_columns_total = show_columns_total, show_rows_total axis = {"columns": 0, "rows": 1} else: axis = {"columns": 1, "rows": 0} @@ -150,16 +582,19 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s # of metrics defined by the user df = df[metrics] - # compute fractions, if needed - if aggfunc.endswith(" as Fraction of Total"): - total = df.sum().sum() - df = df.astype(total.dtypes) / total - elif aggfunc.endswith(" as Fraction of Columns"): - total = df.sum(axis=axis["rows"]) - df = df.astype(total.dtypes).div(total, axis=axis["columns"]) - elif aggfunc.endswith(" as Fraction of Rows"): - total = df.sum(axis=axis["columns"]) - df = df.astype(total.dtypes).div(total, axis=axis["rows"]) + # Compute fractions, if needed. `showValuesAs` supersedes the pre-SIP-216 + # "... as Fraction of ..." aggregate functions, and is applied after the + # totals below so each total divides by its own rollup, as the chart does. + if not percent_mode: + if aggfunc.endswith(" as Fraction of Total"): + total = df.sum().sum() + df = df.astype(total.dtypes) / total + elif aggfunc.endswith(" as Fraction of Columns"): + total = df.sum(axis=axis["rows"]) + df = df.astype(total.dtypes).div(total, axis=axis["columns"]) + elif aggfunc.endswith(" as Fraction of Rows"): + total = df.sum(axis=axis["columns"]) + df = df.astype(total.dtypes).div(total, axis=axis["rows"]) # convert to a MultiIndex to simplify logic if not isinstance(df.index, pd.MultiIndex): @@ -167,6 +602,31 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s if not isinstance(df.columns, pd.MultiIndex): df.columns = pd.MultiIndex.from_tuples([(str(i),) for i in df.columns]) + # Rollups follow each metric's own reducer under a percent mode, so a total + # still divides by itself: a MAX metric's row total is the row's maximum, + # and dividing that maximum by itself reads 100%. + totals_metric_level = df.columns.nlevels - 1 if combine_metrics else 0 + # A column carrying a total label rather than a metric name rolls up + # everything; with no metric to resolve it to, it sums. + cross_metric_reducer = ( + reducers.get(metrics[0], DEFAULT_ROLLUP_REDUCER) + if len(set(metrics)) == 1 + else DEFAULT_ROLLUP_REDUCER + ) + + def collapse(block: pd.DataFrame) -> tuple[pd.DataFrame, str]: + """Narrow a total's source columns to one metric, and pick its reducer.""" + metric_names = set(metrics) + present = [ + _metric_of_column(column, totals_metric_level) for column in block.columns + ] + known = [metric for metric in present if metric in metric_names] + metric = _collapsed_metric(known, metrics) if known else None + if metric is None: + return block, cross_metric_reducer + keep = [column == metric for column in present] + return block.loc[:, keep], reducers.get(str(metric), DEFAULT_ROLLUP_REDUCER) + if show_rows_total: # add subtotal for each group and overall total; we start from the # overall group, and iterate deeper into subgroups @@ -187,12 +647,27 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s subgroups = {group[:level] for group in groups} for subgroup in subgroups: slice_ = df.columns.get_loc(subgroup) - subtotal = pivot_v2_aggfunc_map[aggfunc](df.iloc[:, slice_], axis=1) + block = df.iloc[:, slice_] + if aggfunc != CURRENCY_CONTEXT_AGGREGATION: + # A metric column can hold non-numeric values (a literal + # "NULL", say), which a reduction across columns cannot add + # to a number. The row totals below already coerce; do the + # same here so a total means the same thing on both axes. + block = block.apply(pd.to_numeric, errors="coerce") + if percent_mode: + source, reducer = collapse(block) + subtotal = _reduce(source, reducer, axis=1) + else: + subtotal = pivot_v2_aggfunc_map[aggfunc](block, axis=1) depth = df.columns.nlevels - len(subgroup) - 1 total = metric_name if level == 0 else __("Subtotal") subtotal_name = tuple([*subgroup, total, *([""] * depth)]) # noqa: C409 # insert column after subgroup df.insert(int(slice_.stop), subtotal_name, subtotal) + inserted_columns.append(subtotal_name) + column_prefix_depth[subtotal_name] = ( + level if combine_metrics else max(0, level - 1) + ) if rows and show_columns_total: # add subtotal for each group and overall total; we start from the @@ -216,7 +691,12 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s subtotal_values = subtotal_values.apply( pd.to_numeric, errors="coerce" ) - subtotal = pivot_v2_aggfunc_map[aggfunc](subtotal_values, axis=0) + if percent_mode: + subtotal = subtotal_values.apply( + lambda series: _reduce(series, collapse(series.to_frame())[1]) + ) + else: + subtotal = pivot_v2_aggfunc_map[aggfunc](subtotal_values, axis=0) depth = groups.nlevels - len(subgroup) - 1 total = metric_name if level == 0 else __("Subtotal") subtotal.name = tuple([*subgroup, total, *([""] * depth)]) # noqa: C409 @@ -224,6 +704,46 @@ def pivot_df( # pylint: disable=too-many-locals, too-many-arguments, too-many-s df = pd.concat( [df[: slice_.stop], subtotal.to_frame().T, df[slice_.stop :]] ) + inserted_rows.append(subtotal.name) + row_prefix_depth[subtotal.name] = level + + if percent_mode and rollup_levels: + df = _apply_rollup_totals( + df, + rows, + columns, + metrics, + rollup_levels, + totals_metric_level, + row_prefix_depth, + column_prefix_depth, + ) + + if percent_mode: + df = _apply_show_values_as( + df, + percent_mode, + axis, + metrics, + combine_metrics, + inserted_rows, + inserted_columns, + reducers, + _rollup_denominators( + df, + percent_mode, + rows, + columns, + metrics, + rollup_levels or {}, + totals_metric_level, + row_prefix_depth, + column_prefix_depth, + apply_metrics_on_rows, + ) + if rollup_levels + else None, + ) # if we want to apply the metrics on the rows we need to pivot the # dataframe back @@ -452,6 +972,9 @@ def build_pivot_currency_context( **pivot_options, "aggfunc": CURRENCY_CONTEXT_AGGREGATION, "metric_name_aggfunc": pivot_options["aggfunc"], + # Cells here hold currency-code sets, not numbers, so a percent + # transform would coerce them away. + "show_values_as": None, } return pivot_df(currency_source, **currency_pivot_options) @@ -531,6 +1054,14 @@ def pivot_table_v2( """ verbose_map = datasource.data["verbose_map"] if datasource else None metrics = get_metric_names(form_data["metrics"], verbose_map) + # A non-additive metric makes the chart query every rollup level at once: + # the leaf rows describe the table, the rest are its database-computed + # totals. + df, rollup_levels = split_grouping_sets_levels(df) + show_values_as = form_data.get("showValuesAs") + percent_mode = ( + show_values_as if show_values_as in SHOW_VALUES_AS_PERCENT_MODES else None + ) pivot_options: dict[str, Any] = { "rows": get_column_names(form_data.get("groupbyRows"), verbose_map), "columns": get_column_names(form_data.get("groupbyColumns"), verbose_map), @@ -541,10 +1072,26 @@ def pivot_table_v2( "show_rows_total": bool(form_data.get("rowTotals")), "show_columns_total": bool(form_data.get("colTotals")), "apply_metrics_on_rows": form_data.get("metricsLayout") == "ROWS", + "show_values_as": percent_mode, + "metric_rollup_reducers": get_metric_rollup_reducers( + form_data["metrics"], verbose_map + ), + "rollup_levels": rollup_levels, } pivoted = pivot_df(df, **pivot_options) if apply_number_format: + if percent_mode: + # A ratio has no currency and ignores per-metric value formats, the + # same way the client skips `formattedAggregators` while a fraction + # is active. + return apply_pivot_number_formats( + pivoted, + form_data, + detected_currency, + datasource, + force_number_format=PERCENT_1_POINT, + ) currency_context = None if ( pivot_options["aggfunc"] not in PIVOT_AGGREGATIONS_WITHOUT_CURRENCY_CONTEXT @@ -573,6 +1120,7 @@ def apply_pivot_number_formats( detected_currency: Optional[str] = None, datasource: Optional[Union["BaseDatasource", "Query"]] = None, currency_context: Optional[pd.DataFrame] = None, + force_number_format: Optional[str] = None, ) -> pd.DataFrame: """ Apply `valueFormat`/`columnFormats` and currency config to pivot values. @@ -580,11 +1128,20 @@ def apply_pivot_number_formats( The metric name is the first column level, or the last when `combineMetric` moves it there; in the ROWS metrics layout it is on the index instead. Per-metric overrides fall back to the global value format. + + `force_number_format` applies one d3 format to every metric and drops the + currency config, for values whose configured format no longer applies. """ value_format = form_data.get("valueFormat") column_formats = merge_column_formats(form_data, datasource) currency_format = get_pivot_currency_format(form_data) currency_formats = merge_currency_formats(form_data, datasource) + if force_number_format: + value_format = force_number_format + column_formats = {} + currency_format = {} + currency_formats = {} + currency_context = None metric_level = -1 if form_data.get("combineMetric") else 0 metrics_on_rows = form_data.get("metricsLayout") == "ROWS" @@ -886,6 +1443,15 @@ def apply_client_processing( # noqa: C901 excel.apply_column_types(processed_df, query["coltypes"]) query["data"] = excel.df_to_excel( processed_df, + # A percent mode leaves every cell a fraction. Excel can render + # those as percentages without turning them into text, so the + # workbook reads like the chart and still calculates. + number_format=( + EXCEL_PERCENT_FORMAT + if viz_type == "pivot_table_v2" + and form_data.get("showValuesAs") in SHOW_VALUES_AS_PERCENT_MODES + else None + ), **{ **current_app.config["EXCEL_EXPORT"], "index": show_default_index, diff --git a/superset/constants.py b/superset/constants.py index 3ceede47bdb8..a80f7203f8d1 100644 --- a/superset/constants.py +++ b/superset/constants.py @@ -241,6 +241,26 @@ class PandasAxis(int, Enum): COLUMN = 1 +class ShowValuesAs(StrEnum): + """ + Pivot table "Show values as" modes. + + Mirrors ``ShowValuesAsEnum`` in the pivot table plugin's ``types.ts``. The + value reaches the backend verbatim in the chart's form data, and is honored + by both the pandas postprocessing ``pivot`` operator and the server-side + render of the chart used for exports and reports. + """ + + ACTUAL = "actual" + PERCENT_OF_ROW = "percent_row" + PERCENT_OF_COLUMN = "percent_col" + PERCENT_OF_TOTAL = "percent_total" + + +# The modes that transform values; ``ACTUAL`` (like ``None``) is a no-op. +SHOW_VALUES_AS_PERCENT_MODES = frozenset(ShowValuesAs) - {ShowValuesAs.ACTUAL} + + class PandasPostprocessingCompare(StrEnum): DIFF = "difference" PCT = "percentage" diff --git a/superset/utils/excel.py b/superset/utils/excel.py index a5ea7feed4bc..516ca6186d2c 100644 --- a/superset/utils/excel.py +++ b/superset/utils/excel.py @@ -16,7 +16,7 @@ # under the License. import io from datetime import datetime -from typing import Any +from typing import Any, Optional import pandas as pd @@ -91,7 +91,17 @@ def quote_formulas(df: pd.DataFrame) -> pd.DataFrame: return df.rename_axis(index=_quote_formula, columns=_quote_formula) -def df_to_excel(df: pd.DataFrame, **kwargs: Any) -> Any: +def df_to_excel( + df: pd.DataFrame, number_format: Optional[str] = None, **kwargs: Any +) -> Any: + """ + Serialize a DataFrame to an xlsx workbook. + + :param number_format: optional Excel format code applied to the data + columns, e.g. ``"0.0%"``. Applying it as a cell format rather than + writing formatted text keeps the underlying value numeric, so the + spreadsheet still sums and charts it. + """ output = io.BytesIO() # make sure formulas are quoted, to prevent malicious injections @@ -101,6 +111,20 @@ def df_to_excel(df: pd.DataFrame, **kwargs: Any) -> Any: with pd.ExcelWriter(output, engine="xlsxwriter") as writer: df.to_excel(writer, **kwargs) + if number_format and writer.sheets: + worksheet = next(iter(writer.sheets.values())) + # The sheet may start past column A, and the index occupies the + # leading columns when it is written out. + first_data_column = kwargs.get("startcol", 0) + ( + df.index.nlevels if kwargs.get("index", True) else 0 + ) + worksheet.set_column( + first_data_column, + first_data_column + len(df.columns) - 1, + None, + writer.book.add_format({"num_format": number_format}), + ) + # Reset workbook document properties so the exported file does not # carry identifying details (authoring info, generation timestamps). writer.book.set_properties(NEUTRAL_DOCUMENT_PROPERTIES) diff --git a/superset/utils/pandas_postprocessing/pivot.py b/superset/utils/pandas_postprocessing/pivot.py index e18382935a38..722def27dbb6 100644 --- a/superset/utils/pandas_postprocessing/pivot.py +++ b/superset/utils/pandas_postprocessing/pivot.py @@ -20,15 +20,18 @@ from flask_babel import gettext as _ from pandas import DataFrame -from superset.constants import NULL_STRING, PandasAxis +from superset.constants import ( + NULL_STRING, + PandasAxis, + SHOW_VALUES_AS_PERCENT_MODES, + ShowValuesAs, +) from superset.exceptions import InvalidPostProcessingError from superset.utils.pandas_postprocessing.utils import ( _get_aggregate_funcs, validate_column_args, ) -_PERCENT_MODES = frozenset({"percent_row", "percent_col", "percent_total"}) - # Aggregate operator names that produce additive results across groups — # the sum of the per-cell values equals the row/column/grand rollup the # database would compute over the underlying rows. ``show_values_as`` @@ -60,10 +63,10 @@ def _apply_percent_transform_to_group(g: DataFrame, mode: str) -> DataFrame: from division-by-zero, matching the client's ``if (acc === null) return null`` guard in ``fractionOf``. """ - if mode == "percent_row": + if mode == ShowValuesAs.PERCENT_OF_ROW: row_totals = g.sum(axis=PandasAxis.COLUMN, skipna=True).replace(0, float("nan")) return _div_preserving_nan(g, row_totals, axis=PandasAxis.ROW) - if mode == "percent_col": + if mode == ShowValuesAs.PERCENT_OF_COLUMN: col_totals = g.sum(axis=PandasAxis.ROW, skipna=True).replace(0, float("nan")) return _div_preserving_nan(g, col_totals, axis=PandasAxis.COLUMN) # percent_total @@ -250,8 +253,8 @@ def pivot( # pylint: disable=too-many-arguments # noqa: C901 # ``""`` / ``"actual"`` are the no-op sentinels — anything else must # be a known percent mode. percent_mode: Optional[str] = None - if show_values_as not in (None, "", "actual"): - if show_values_as not in _PERCENT_MODES: + if show_values_as not in (None, "", ShowValuesAs.ACTUAL): + if show_values_as not in SHOW_VALUES_AS_PERCENT_MODES: raise InvalidPostProcessingError( _( "Unsupported show_values_as value: %(mode)s. " diff --git a/tests/unit_tests/charts/test_client_processing.py b/tests/unit_tests/charts/test_client_processing.py index d5c437e07954..1d3bbca6f49b 100644 --- a/tests/unit_tests/charts/test_client_processing.py +++ b/tests/unit_tests/charts/test_client_processing.py @@ -16,6 +16,7 @@ # under the License. from io import BytesIO, StringIO +from typing import Any from unittest.mock import MagicMock import pandas as pd @@ -522,11 +523,11 @@ def test_pivot_df_single_row_null_values(): assert ( pivoted.to_markdown() == """ -| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | -|:-----------------|----------------:|----------------:|:-------------------| -| ('boy',) | nan | nan | nannan | -| ('girl',) | 118065 | 2588 | 120653.0 | -| ('Total (Sum)',) | 118065 | 2588 | 120653.0 | +| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | +|:-----------------|----------------:|----------------:|-------------------:| +| ('boy',) | nan | nan | 0 | +| ('girl',) | 118065 | 2588 | 120653 | +| ('Total (Sum)',) | 118065 | 2588 | 120653 | """.strip() ) @@ -546,15 +547,12 @@ def test_pivot_df_single_row_null_values(): assert ( pivoted.to_markdown() == f""" -| | ('{_("Total")} (Sum)',) | -|:-------------------------|-------------------:| -| ('SUM(num)', 'boy') | nan | -| ('SUM(num)', 'girl') | 118065 | -| ('SUM(num)', 'Subtotal') | 118065 | -| ('MAX(num)', 'boy') | nan | -| ('MAX(num)', 'girl') | 2588 | -| ('MAX(num)', 'Subtotal') | 2588 | -| ('{_("Total")} (Sum)', '') | 120653 | +| | ('{_("Total")} (Sum)',) | +|:---------------------|-------------------:| +| ('SUM(num)', 'boy') | nan | +| ('SUM(num)', 'girl') | 118065 | +| ('MAX(num)', 'boy') | nan | +| ('MAX(num)', 'girl') | 2588 | """.strip() ) @@ -692,11 +690,11 @@ def test_pivot_df_single_row_null_mix_values_strings(): assert ( pivoted.to_markdown() == """ -| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | -|:-----------------|:----------------|----------------:|:-------------------| -| ('boy',) | NULL | nan | NULLnan | -| ('girl',) | 118065 | 2588 | 120653.0 | -| ('Total (Sum)',) | 118065.0 | 2588 | 120653.0 | +| | ('SUM(num)',) | ('MAX(num)',) | ('Total (Sum)',) | +|:-----------------|:----------------|----------------:|-------------------:| +| ('boy',) | NULL | nan | 0 | +| ('girl',) | 118065 | 2588 | 120653 | +| ('Total (Sum)',) | 118065.0 | 2588 | 120653 | """.strip() ) @@ -720,8 +718,11 @@ def test_pivot_df_single_row_null_mix_values_strings(): |:---------------------|:-------------------| | ('boy', 'SUM(num)') | NULL | | ('boy', 'MAX(num)') | nan | +| ('boy', 'Subtotal') | 0.0 | | ('girl', 'SUM(num)') | 118065 | | ('girl', 'MAX(num)') | 2588.0 | +| ('girl', 'Subtotal') | 120653.0 | +| ('Total (Sum)', '') | 120653.0 | """.strip() ) @@ -853,12 +854,15 @@ def test_pivot_df_single_row_null_mix_values_numbers(): assert ( pivoted.to_markdown() == """ -| | ('Total (Sum)',) | -|:---------------------|-------------------:| -| ('SUM(num)', 'boy') | 21 | -| ('SUM(num)', 'girl') | 118065 | -| ('MAX(num)', 'boy') | nan | -| ('MAX(num)', 'girl') | 2588 | +| | ('Total (Sum)',) | +|:-------------------------|-------------------:| +| ('SUM(num)', 'boy') | 21 | +| ('SUM(num)', 'girl') | 118065 | +| ('SUM(num)', 'Subtotal') | 118086 | +| ('MAX(num)', 'boy') | nan | +| ('MAX(num)', 'girl') | 2588 | +| ('MAX(num)', 'Subtotal') | 2588 | +| ('Total (Sum)', '') | 120674 | """.strip() ) @@ -882,8 +886,11 @@ def test_pivot_df_single_row_null_mix_values_numbers(): |:---------------------|-------------------:| | ('boy', 'SUM(num)') | 21 | | ('boy', 'MAX(num)') | nan | +| ('boy', 'Subtotal') | 21 | | ('girl', 'SUM(num)') | 118065 | | ('girl', 'MAX(num)') | 2588 | +| ('girl', 'Subtotal') | 120653 | +| ('{_("Total")} (Sum)', '') | 120674 | """.strip() ) @@ -1804,6 +1811,558 @@ def test_pivot_df_complex_null_values(): ) +# --- `showValuesAs` percent modes (#42809) ----------------------------------- +# +# Exports and scheduled reports render server-side, so they have to reproduce +# the client's `fractionOf` aggregator: each cell over its row, column, or grand +# total, computed per metric, with totals dividing by their own rollup rather +# than summing the fractions around them. + +SHOW_VALUES_AS_OPTIONS: dict[str, Any] = { + "rows": ["nation"], + "columns": ["gender"], + "metrics": ["SUM(num)"], + "aggfunc": "Sum", + "transpose_pivot": False, + "combine_metrics": False, + "show_rows_total": True, + "show_columns_total": True, + "apply_metrics_on_rows": False, +} + + +def show_values_as_df() -> pd.DataFrame: + """A 2x2 pivot: row totals 40/40, column totals 30/50, grand total 80.""" + return pd.DataFrame( + { + "nation": ["US", "US", "UK", "UK"], + "gender": ["boy", "girl", "boy", "girl"], + "SUM(num)": [10, 30, 20, 20], + } + ) + + +def total_label() -> str: + return f"{_('Total')} (Sum)" + + +def test_pivot_df_show_values_as_percent_row(): + pivoted = pivot_df( + show_values_as_df(), **SHOW_VALUES_AS_OPTIONS, show_values_as="percent_row" + ) + total = total_label() + + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.25 + assert pivoted.loc[("US",), ("SUM(num)", "girl")] == 0.75 + assert pivoted.loc[("UK",), ("SUM(num)", "boy")] == 0.5 + # a row is always 100% of itself + assert pivoted.loc[("US",), (total, "")] == 1 + # the totals row shows each column's share of the grand total (30/80 and + # 50/80), not the sum of the fractions above it + assert pivoted.loc[(total,), ("SUM(num)", "boy")] == 0.375 + assert pivoted.loc[(total,), ("SUM(num)", "girl")] == 0.625 + assert pivoted.loc[(total,), (total, "")] == 1 + + +def test_pivot_df_show_values_as_percent_col(): + pivoted = pivot_df( + show_values_as_df(), **SHOW_VALUES_AS_OPTIONS, show_values_as="percent_col" + ) + total = total_label() + + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == pytest.approx(1 / 3) + assert pivoted.loc[("UK",), ("SUM(num)", "boy")] == pytest.approx(2 / 3) + assert pivoted.loc[("US",), ("SUM(num)", "girl")] == 0.6 + # a column is always 100% of itself + assert pivoted.loc[(total,), ("SUM(num)", "boy")] == 1 + # the totals column shows each row's share of the grand total + assert pivoted.loc[("US",), (total, "")] == 0.5 + + +def test_pivot_df_show_values_as_percent_total(): + pivoted = pivot_df( + show_values_as_df(), **SHOW_VALUES_AS_OPTIONS, show_values_as="percent_total" + ) + total = total_label() + + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.125 + assert pivoted.loc[("US",), ("SUM(num)", "girl")] == 0.375 + assert pivoted.loc[("UK",), ("SUM(num)", "boy")] == 0.25 + assert pivoted.loc[(total,), ("SUM(num)", "boy")] == 0.375 + assert pivoted.loc[("US",), (total, "")] == 0.5 + assert pivoted.loc[(total,), (total, "")] == 1 + + +def test_pivot_df_show_values_as_keeps_metrics_separate(): + """One metric's cells are never divided by another metric's total.""" + df = show_values_as_df() + df["MAX(num)"] = [1, 3, 6, 10] + pivoted = pivot_df( + df, + **{**SHOW_VALUES_AS_OPTIONS, "metrics": ["SUM(num)", "MAX(num)"]}, + show_values_as="percent_row", + ) + + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.25 + assert pivoted.loc[("UK",), ("MAX(num)", "boy")] == 0.375 + assert pivoted.loc[("UK",), ("MAX(num)", "girl")] == 0.625 + # a total collapsing the metric axis resolves to one metric, as the + # renderer does, so it still divides by itself + assert pivoted.loc[("US",), (total_label(), "")] == 1 + + +def test_pivot_df_show_values_as_with_combined_metrics(): + """`combineMetric` moves the metric to the lowest column level.""" + df = show_values_as_df() + df["MAX(num)"] = [1, 3, 6, 10] + pivoted = pivot_df( + df, + **{ + **SHOW_VALUES_AS_OPTIONS, + "metrics": ["SUM(num)", "MAX(num)"], + "combine_metrics": True, + "show_rows_total": False, + "show_columns_total": False, + }, + show_values_as="percent_row", + ) + + assert pivoted.loc[("US",), ("boy", "SUM(num)")] == 0.25 + assert pivoted.loc[("US",), ("girl", "SUM(num)")] == 0.75 + # each metric keeps its own denominator across the combined layout + assert pivoted.loc[("UK",), ("boy", "MAX(num)")] == 0.375 + assert pivoted.loc[("UK",), ("girl", "MAX(num)")] == 0.625 + + +def test_pivot_table_v2_show_values_as_uses_min_max_rollups(): + """A MIN/MAX metric divides by the row's extreme, not its sum. + + Mirrors `additiveReducerFor` in the plugin's `plugin/utilities.ts`: the + chart rolls a MAX metric up with max, so a row of [6, 10] reads 60%/100%. + """ + df = pd.DataFrame( + { + "nation": ["US", "US", "UK", "UK"], + "gender": ["boy", "girl", "boy", "girl"], + "MAX(num)": [1, 3, 6, 10], + } + ) + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": [ + { + "expressionType": "SIMPLE", + "aggregate": "MAX", + "column": {"column_name": "num"}, + "label": "MAX(num)", + } + ], + "showValuesAs": "percent_row", + "rowTotals": True, + } + + pivoted = pivot_table_v2(df, form_data, apply_number_format=False) + + assert pivoted.loc[("UK",), ("MAX(num)", "boy")] == 0.6 + assert pivoted.loc[("UK",), ("MAX(num)", "girl")] == 1 + # the total divides by itself whatever the reducer + assert pivoted.loc[("UK",), (total_label(), "")] == 1 + + +def grouping_sets_df() -> pd.DataFrame: + """A GROUPING SETS result whose rollups differ from any leaf reduction. + + Leaves are 10 and 20, so a leaf-derived total would be 30 (sum) or 15 + (mean). The database rollups are deliberately none of those: 18 down the + column, 11/21 across the rows, 19 overall -- as a weighted average or a + distinct count would be. + """ + return pd.DataFrame( + { + "nation": ["US", "UK", None, "US", "UK", None], + "gender": ["boy", "boy", "boy", None, None, None], + "nation__superset_grouping": [0, 0, 1, 0, 0, 1], + "gender__superset_grouping": [0, 0, 0, 1, 1, 1], + "AVG(num)": [10, 20, 18, 11, 21, 19], + } + ) + + +def nested_grouping_sets_df() -> pd.DataFrame: + """Two row dimensions, so prefix subtotal rows appear beside the rollups. + + Every level holds a value no leaf reduction would produce: the `{region}` + rollups are 17 and 24, `{region, nation}` are 18 and 25, `{gender}` are 26 + and 24, and the grand total is 21. + """ + records = [ + # (region, nation, gender, rolled-up markers, value) + ("EU", "UK", "boy", (0, 0, 0), 10), + ("EU", "UK", "girl", (0, 0, 0), 30), + ("NA", "US", "boy", (0, 0, 0), 40), + ("NA", "US", "girl", (0, 0, 0), 20), + ("EU", "UK", None, (0, 0, 1), 18), + ("NA", "US", None, (0, 0, 1), 25), + ("EU", None, "boy", (0, 1, 0), 10), + ("EU", None, "girl", (0, 1, 0), 30), + ("NA", None, "boy", (0, 1, 0), 40), + ("NA", None, "girl", (0, 1, 0), 20), + ("EU", None, None, (0, 1, 1), 17), + ("NA", None, None, (0, 1, 1), 24), + (None, None, "boy", (1, 1, 0), 26), + (None, None, "girl", (1, 1, 0), 24), + (None, None, None, (1, 1, 1), 21), + ] + return pd.DataFrame( + [ + { + "region": region, + "nation": nation, + "gender": gender, + "region__superset_grouping": markers[0], + "nation__superset_grouping": markers[1], + "gender__superset_grouping": markers[2], + "AVG(num)": value, + } + for region, nation, gender, markers, value in records + ] + ) + + +NESTED_FORM_DATA = { + "groupbyRows": ["region", "nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_row", + "rowTotals": True, + "colTotals": True, +} + + +def test_row_subtotal_keeps_its_total_column_cell(): + """A subtotal row is not a whole-axis total; it must not be looked up as one.""" + pivoted = pivot_table_v2( + nested_grouping_sets_df(), NESTED_FORM_DATA, apply_number_format=False + ) + + assert not pd.isna(pivoted.loc[("EU", "Subtotal"), (total_label(), "")]) + + +def test_row_subtotal_divides_by_its_own_rollup(): + """An "EU" subtotal divides by the {region} rollup, not the grand total.""" + pivoted = pivot_table_v2( + nested_grouping_sets_df(), NESTED_FORM_DATA, apply_number_format=False + ) + + assert pivoted.loc[("EU", "Subtotal"), ("AVG(num)", "boy")] == pytest.approx( + 10 / 17 + ) + assert pivoted.loc[("NA", "Subtotal"), ("AVG(num)", "boy")] == pytest.approx( + 40 / 24 + ) + + +def test_nested_leaf_rows_still_divide_by_database_rollups(): + """Regression guard: the leaf and whole-axis behaviour is unchanged.""" + pivoted = pivot_table_v2( + nested_grouping_sets_df(), NESTED_FORM_DATA, apply_number_format=False + ) + + assert pivoted.loc[("EU", "UK"), ("AVG(num)", "boy")] == pytest.approx(10 / 18) + assert pivoted.loc[("NA", "US"), ("AVG(num)", "boy")] == pytest.approx(40 / 25) + assert pivoted.loc[("EU", "UK"), (total_label(), "")] == 1 + assert pivoted.loc[(total_label(), ""), (total_label(), "")] == 1 + + +def test_pivot_table_v2_pivots_only_grouping_sets_leaf_rows(): + """Rollup levels must not be pivoted as if they were leaf rows.""" + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_col", + } + + pivoted = pivot_table_v2(grouping_sets_df(), form_data, apply_number_format=False) + + # the rollup rows would otherwise add phantom rows for their NULL dimensions + assert len(pivoted.index) == 2 + + +@pytest.mark.parametrize( + "mode,expected", + [ + # each leaf divides by the rollup the database computed, never by a + # total re-derived from the leaves + ("percent_col", 20 / 18), + ("percent_row", 20 / 21), + ("percent_total", 20 / 19), + ], +) +def test_pivot_table_v2_divides_by_database_rollups(mode: str, expected: float): + """For a non-additive metric the DB rollup is the only correct total.""" + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": mode, + } + + pivoted = pivot_table_v2(grouping_sets_df(), form_data, apply_number_format=False) + + assert pivoted.loc[("UK",), ("AVG(num)", "boy")] == pytest.approx(expected) + + +def _axis_has_total(axis) -> bool: + return any( + total_label() in (label if isinstance(label, tuple) else (label,)) + for label in axis + ) + + +@pytest.mark.parametrize("layout", ["COLUMNS", "ROWS"]) +@pytest.mark.parametrize("toggle", ["rowTotals", "colTotals"]) +def test_pivot_table_v2_totals_land_on_the_displayed_axis(layout: str, toggle: str): + """`rowTotals` is always the right-hand column, whatever the metrics layout. + + Moving metrics to rows transposes the frame on the way out, which would + otherwise flip the axis each total was inserted on. + """ + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_row", + "metricsLayout": layout, + toggle: True, + } + + pivoted = pivot_table_v2(grouping_sets_df(), form_data, apply_number_format=False) + + assert _axis_has_total(pivoted.columns) is (toggle == "rowTotals") + assert _axis_has_total(pivoted.index) is (toggle == "colTotals") + + +def test_pivot_table_v2_uses_database_rollups_with_metrics_on_rows(): + """The metrics layout moves the displayed axes but not the rollup levels.""" + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_col", + "metricsLayout": "ROWS", + } + + pivoted = pivot_table_v2(grouping_sets_df(), form_data, apply_number_format=False) + + # 20/18 and 10/18 from the database rollup, not 20/30 from the leaf sum + assert pivoted.loc[("AVG(num)", "UK"), ("boy",)] == pytest.approx(20 / 18) + assert pivoted.loc[("AVG(num)", "US"), ("boy",)] == pytest.approx(10 / 18) + + +def test_pivot_table_v2_keeps_a_null_rollup_blank(): + """A NULL rollup is not a missing one: it blanks the cell, as the chart does.""" + df = grouping_sets_df() + df.loc[df["nation"].isna() & df["gender"].notna(), "AVG(num)"] = None + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_col", + } + + pivoted = pivot_table_v2(df, form_data, apply_number_format=False) + + # falling back to the leaf-derived total would have produced 10/30 here + assert pd.isna(pivoted.loc[("US",), ("AVG(num)", "boy")]) + + +def test_pivot_table_v2_rollup_totals_divide_by_themselves(): + """A whole-axis total takes its value from the same level as its denominator.""" + form_data = { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["AVG(num)"], + "showValuesAs": "percent_row", + "rowTotals": True, + } + + pivoted = pivot_table_v2(grouping_sets_df(), form_data, apply_number_format=False) + + # 21/21, not the leaf sum over the database row rollup + assert pivoted.loc[("UK",), (total_label(), "")] == 1 + + +def test_pivot_df_show_values_as_metrics_on_rows(): + pivoted = pivot_df( + show_values_as_df(), + **{**SHOW_VALUES_AS_OPTIONS, "apply_metrics_on_rows": True}, + show_values_as="percent_col", + ) + total = total_label() + + assert pivoted.loc[("SUM(num)", "US"), ("boy",)] == pytest.approx(1 / 3) + assert pivoted.loc[("SUM(num)", "US"), ("girl",)] == 0.6 + assert pivoted.loc[("SUM(num)", "US"), (total,)] == 0.5 + assert pivoted.loc[(total, ""), ("boy",)] == 1 + + +def test_pivot_df_show_values_as_zero_denominator_is_blank(): + """A zero total renders blank rather than infinity.""" + df = show_values_as_df() + df.loc[df["nation"] == "UK", "SUM(num)"] = 0 + pivoted = pivot_df(df, **SHOW_VALUES_AS_OPTIONS, show_values_as="percent_row") + + assert pd.isna(pivoted.loc[("UK",), ("SUM(num)", "boy")]) + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.25 + + +def test_pivot_df_show_values_as_skips_empty_cells_in_denominators(): + """A sparse pivot leaves NaN cells; they must not blank the whole table.""" + df = show_values_as_df() + # drop UK/girl entirely, so that cell has no rows and pivots to NaN + sparse = df[~((df["nation"] == "UK") & (df["gender"] == "girl"))] + options = {**SHOW_VALUES_AS_OPTIONS, "show_rows_total": False} + + pivoted = pivot_df(sparse, **options, show_values_as="percent_total") + + # grand total is 10 + 30 + 20 = 60, the NaN cell contributing nothing + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == pytest.approx(10 / 60) + assert pivoted.loc[("UK",), ("SUM(num)", "boy")] == pytest.approx(20 / 60) + assert pd.isna(pivoted.loc[("UK",), ("SUM(num)", "girl")]) + + +@pytest.mark.parametrize("aggfunc", ["Sum", "Average", "Maximum"]) +def test_pivot_df_show_values_as_ignores_legacy_aggregate_function(aggfunc: str): + """A total always divides by itself, whatever `aggregateFunction` says. + + The chart ignores `aggregateFunction` post-SIP-216, so a percent export must + too: computing totals with it while the denominators sum leaves the Total + row/column reading something other than 100%. + """ + pivoted = pivot_df( + show_values_as_df(), + **{**SHOW_VALUES_AS_OPTIONS, "aggfunc": aggfunc}, + show_values_as="percent_row", + ) + total = total_label() + + assert pivoted.loc[("US",), (total, "")] == 1 + assert pivoted.loc[(total,), (total, "")] == 1 + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.25 + + +def test_pivot_df_show_values_as_supersedes_legacy_fraction_aggfunc(): + """`showValuesAs` wins over a pre-SIP-216 fraction aggregate function.""" + pivoted = pivot_df( + show_values_as_df(), + **{**SHOW_VALUES_AS_OPTIONS, "aggfunc": "Sum as Fraction of Total"}, + show_values_as="percent_row", + ) + + assert pivoted.loc[("US",), ("SUM(num)", "boy")] == 0.25 + + +def test_pivot_table_v2_show_values_as_formats_as_percent() -> None: + """A ratio ignores the configured value format and currency, as the chart does.""" + result = pivot_table_v2( + show_values_as_df(), + { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["SUM(num)"], + "showValuesAs": "percent_total", + "valueFormat": "$,.2f", + "currencyFormat": {"symbol": "USD", "symbolPosition": "prefix"}, + }, + ) + + assert result.loc[("US",), ("SUM(num)", "boy")] == "12.5%" + assert result.loc[("US",), ("SUM(num)", "girl")] == "37.5%" + + +def test_pivot_table_v2_show_values_as_actual_keeps_raw_values() -> None: + result = pivot_table_v2( + show_values_as_df(), + { + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["SUM(num)"], + "showValuesAs": "actual", + "valueFormat": ",d", + }, + ) + + assert result.loc[("US",), ("SUM(num)", "boy")] == "10" + + +def test_apply_client_processing_xlsx_formats_percentages_as_cells(): + """The workbook shows percentages without giving up numeric cells. + + A scheduled XLSX report and the browser's pivoted-Excel download have to + agree; writing formatted text would match the download but stop Excel from + summing the column. + """ + import openpyxl # noqa: PLC0415 + + source = pd.DataFrame( + {"nation": ["US", "US"], "gender": ["boy", "girl"], "SUM(num)": [1, 99]} + ) + result = { + "queries": [ + { + "result_format": ChartDataResultFormat.XLSX, + "data": excel.df_to_excel(source, index=False), + } + ] + } + form_data = { + "viz_type": "pivot_table_v2", + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["SUM(num)"], + "showValuesAs": "percent_row", + "metricsLayout": "COLUMNS", + } + + processed = apply_client_processing(result, form_data) + + workbook = openpyxl.load_workbook(BytesIO(processed["queries"][0]["data"])) + sheet = workbook.active + assert sheet.cell(row=2, column=2).value == pytest.approx(0.01) + assert sheet.cell(row=2, column=2).number_format == "0.0%" + + +def test_apply_client_processing_csv_format_show_values_as(): + """CSV exports carry the percentages the chart displays.""" + result = { + "queries": [ + { + "result_format": ChartDataResultFormat.CSV, + "data": ( + "nation,gender,SUM(num)\n" + "US,boy,10\nUS,girl,30\nUK,boy,20\nUK,girl,20\n" + ), + } + ] + } + form_data = { + "viz_type": "pivot_table_v2", + "groupbyRows": ["nation"], + "groupbyColumns": ["gender"], + "metrics": ["SUM(num)"], + "showValuesAs": "percent_row", + "metricsLayout": "COLUMNS", + } + + processed = apply_client_processing(result, form_data) + + assert processed["queries"][0]["data"] == ( + ",SUM(num) boy,SUM(num) girl\nUK,0.5,0.5\nUS,0.25,0.75\n" + ) + + def test_table(): """ Test that the table reports honor `d3NumberFormat`. From ba0142ca6446a7d5106faaadd04456138d949cff Mon Sep 17 00:00:00 2001 From: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:32:30 +0200 Subject: [PATCH 02/12] fix(dashboard): reconcile stale results tab in Chart Data modal (#43648) Co-authored-by: Claude Sonnet 5 --- .../DataTablesPane/DataTablesPane.tsx | 26 +-------- .../components/ResultsPaneOnDashboard.tsx | 7 +++ .../test/ResultsPaneOnDashboard.test.tsx | 58 +++++++++++++++++++ .../test/getStaleResultsTabFallback.test.ts | 2 +- .../components/DataTablesPane/utils.ts | 54 +++++++++++++++++ 5 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 superset-frontend/src/explore/components/DataTablesPane/utils.ts diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx index cb8878e3bf97..f3c505366e2d 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx @@ -29,22 +29,7 @@ import { } from 'src/utils/localStorageHelpers'; import { SamplesPane, useResultsPane } from './components'; import { DataTablesPaneProps, ResultTypes } from './types'; - -/** - * A mixed chart can be reconfigured to return fewer result panes than before - * (e.g. dropping a query), which removes the corresponding results tab. If the - * selected tab was one of those, the active key goes stale and the data panel - * renders blank until the user reselects a valid tab. Returns the first - * results tab to fall back to in that case, otherwise undefined. - */ -export const getStaleResultsTabFallback = ( - activeTabKey: string, - resultsTabKeys: string[], -): string | undefined => - activeTabKey.startsWith(ResultTypes.Results) && - !resultsTabKeys.includes(activeTabKey) - ? ResultTypes.Results - : undefined; +import { useStaleResultsTabFallback } from './utils'; const StyledDiv = styled.div` ${() => ` @@ -230,17 +215,12 @@ export const DataTablesPane = ({ }; }); - const resultsTabFallback = getStaleResultsTabFallback( + useStaleResultsTabFallback( activeTabKey, queryResultsPanes.map(({ key }) => key), + setActiveTabKey, ); - useEffect(() => { - if (resultsTabFallback) { - setActiveTabKey(resultsTabFallback); - } - }, [resultsTabFallback]); - // Hide the Samples tab for datasources that don't expose raw rows // (e.g. semantic views). The check is intentionally ``=== false`` so that // datasources from older backends that don't send the flag still show the diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx index 4485ca792081..510d6edf5034 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx @@ -20,6 +20,7 @@ import { t } from '@apache-superset/core/translation'; import { styled } from '@apache-superset/core/theme'; import Tabs from '@superset-ui/core/components/Tabs'; import { ResultTypes, ResultsPaneProps } from '../types'; +import { useStaleResultsTabFallback } from '../utils'; import { useResultsPane } from './useResultsPane'; import { useState } from 'react'; @@ -86,6 +87,12 @@ export const ResultsPaneOnDashboard = ({ }; }); + useStaleResultsTabFallback( + activeTabKey, + items.map(({ key }) => key), + setActiveTabKey, + ); + return ( diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx index 145ab3e78962..d0b02af4f614 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -20,14 +20,36 @@ import fetchMock from 'fetch-mock'; import { screen, render, + act, waitForElementToBeRemoved, waitFor, } from 'spec/helpers/testing-library'; import { ChartMetadata, ChartPlugin, VizType } from '@superset-ui/core'; import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact'; +import Tabs from '@superset-ui/core/components/Tabs'; import { ResultsPaneOnDashboard } from '../components'; +import { useResultsPane } from '../components/useResultsPane'; import { createResultsPaneOnDashboardProps } from './fixture'; +jest.mock('@superset-ui/core/components/Tabs', () => { + const actual = jest.requireActual('@superset-ui/core/components/Tabs'); + return { __esModule: true, ...actual, default: jest.fn(actual.default) }; +}); + +// Wraps the real hook; only overridden below to avoid mounting a second +// real AG Grid instance, which jsdom doesn't support. +jest.mock('../components/useResultsPane', () => { + const actual = jest.requireActual('../components/useResultsPane'); + return { + __esModule: true, + useResultsPane: jest.fn(actual.useResultsPane), + }; +}); + +const actualUseResultsPane = jest.requireActual( + '../components/useResultsPane', +).useResultsPane; + beforeAll(() => { setupAGGridModules(); }); @@ -93,6 +115,10 @@ describe('ResultsPaneOnDashboard', () => { const setForceQuery = jest.fn(); + afterEach(() => { + (useResultsPane as jest.Mock).mockImplementation(actualUseResultsPane); + }); + afterAll(() => { fetchMock.clearHistory().removeRoutes(); jest.resetAllMocks(); @@ -219,4 +245,36 @@ describe('ResultsPaneOnDashboard', () => { expect(tab2).toBeVisible(); expect(tab3).toBeNull(); }); + + test('falls back to the first results tab when the active one disappears', async () => { + const mockedUseResultsPane = useResultsPane as jest.Mock; + mockedUseResultsPane.mockReturnValue([
,
]); + + const props = createResultsPaneOnDashboardProps({ sliceId: 999 }); + const { rerender } = render(, { + useRedux: true, + }); + + const latestTabsProps = () => { + const { calls } = (Tabs as unknown as jest.Mock).mock; + return calls[calls.length - 1][0]; + }; + expect(latestTabsProps().items.map((i: { key: string }) => i.key)).toEqual([ + 'results', + 'results 2', + ]); + + act(() => { + latestTabsProps().onChange('results 2'); + }); + expect(latestTabsProps().activeKey).toBe('results 2'); + + // A mixed chart dropped from two query results to one, removing "results 2" + mockedUseResultsPane.mockReturnValue([
]); + rerender(); + + await waitFor(() => { + expect(latestTabsProps().activeKey).toBe('results'); + }); + }); }); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/getStaleResultsTabFallback.test.ts b/superset-frontend/src/explore/components/DataTablesPane/test/getStaleResultsTabFallback.test.ts index d387babf4ee0..2b6d8df6aee2 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/getStaleResultsTabFallback.test.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/test/getStaleResultsTabFallback.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { getStaleResultsTabFallback } from '../DataTablesPane'; +import { getStaleResultsTabFallback } from '../utils'; import { ResultTypes } from '../types'; test('keeps the active tab when it still exists', () => { diff --git a/superset-frontend/src/explore/components/DataTablesPane/utils.ts b/superset-frontend/src/explore/components/DataTablesPane/utils.ts new file mode 100644 index 000000000000..bfd984df9f78 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/utils.ts @@ -0,0 +1,54 @@ +/** + * 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 { useEffect } from 'react'; +import { ResultTypes } from './types'; + +/** + * A mixed chart can be reconfigured to return fewer result panes than before + * (e.g. dropping a query), which removes the corresponding results tab. If the + * selected tab was one of those, the active key goes stale and the data panel + * renders blank until the user reselects a valid tab. Returns the first + * results tab to fall back to in that case, otherwise undefined. + */ +export const getStaleResultsTabFallback = ( + activeTabKey: string, + resultsTabKeys: string[], +): string | undefined => + activeTabKey.startsWith(ResultTypes.Results) && + !resultsTabKeys.includes(activeTabKey) + ? ResultTypes.Results + : undefined; + +/** + * Switches the active tab back to the first results tab when it goes stale, + * per `getStaleResultsTabFallback`. + */ +export const useStaleResultsTabFallback = ( + activeTabKey: string, + tabKeys: string[], + setActiveTabKey: (key: string) => void, +) => { + const fallback = getStaleResultsTabFallback(activeTabKey, tabKeys); + + useEffect(() => { + if (fallback) { + setActiveTabKey(fallback); + } + }, [fallback, setActiveTabKey]); +}; From b04a15345147d328f0c678079ac7af48c051efde Mon Sep 17 00:00:00 2001 From: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:05:27 +0200 Subject: [PATCH 03/12] fix(dashboard): stretch Chart Data modal results grid to fill available height (#43454) Co-authored-by: Claude Sonnet 5 --- .../components/ResultsPaneOnDashboard.tsx | 19 ++++++------------- .../test/ResultsPaneOnDashboard.test.tsx | 3 +++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx index 510d6edf5034..9a8c6123bcbd 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx @@ -29,22 +29,10 @@ const Wrapper = styled.div` flex-direction: column; height: 100%; - .ant-tabs { - height: 100%; - } - - .ant-tabs-body { - height: 100%; - } - .ant-tabs-content { display: flex; flex-direction: column; } - - .table-condensed { - overflow: auto; - } `; export const ResultsPaneOnDashboard = ({ @@ -95,7 +83,12 @@ export const ResultsPaneOnDashboard = ({ return ( - + ); }; diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx index d0b02af4f614..aab837f663f6 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -132,6 +132,9 @@ describe('ResultsPaneOnDashboard', () => { expect( await findByText('No results were returned for this query'), ).toBeVisible(); + const tabsMock = Tabs as unknown as jest.Mock; + const [tabsProps] = tabsMock.mock.calls[tabsMock.mock.calls.length - 1]; + expect(tabsProps).toEqual(expect.objectContaining({ fullHeight: true })); }); test('render errorMessage', async () => { From e3d30bb730e2ad1a4357ed463fa4d5c2f6181e9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:49:50 +0700 Subject: [PATCH 04/12] chore(deps-dev): bump oxlint from 1.79.0 to 1.80.0 in /superset-frontend (#43753) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 160 ++++++++++++++-------------- superset-frontend/package.json | 2 +- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 0bf3828a0067..cc2b61d68bca 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -255,7 +255,7 @@ "minimizer-webpack-plugin": "^5.6.1", "open-cli": "^9.0.0", "oxfmt": "^0.64.0", - "oxlint": "^1.79.0", + "oxlint": "^1.80.0", "po2json": "^0.4.5", "postcss-styled-syntax": "^0.7.2", "process": "^0.11.10", @@ -8826,9 +8826,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", - "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz", + "integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==", "cpu": [ "arm" ], @@ -8843,9 +8843,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", - "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz", + "integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==", "cpu": [ "arm64" ], @@ -8860,9 +8860,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", - "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz", + "integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==", "cpu": [ "arm64" ], @@ -8877,9 +8877,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", - "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz", + "integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==", "cpu": [ "x64" ], @@ -8894,9 +8894,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", - "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz", + "integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==", "cpu": [ "x64" ], @@ -8911,9 +8911,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", - "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz", + "integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==", "cpu": [ "arm" ], @@ -8928,9 +8928,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", - "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz", + "integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==", "cpu": [ "arm" ], @@ -8945,9 +8945,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", - "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz", + "integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==", "cpu": [ "arm64" ], @@ -8965,9 +8965,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", - "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz", + "integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==", "cpu": [ "arm64" ], @@ -8985,9 +8985,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", - "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz", + "integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==", "cpu": [ "ppc64" ], @@ -9005,9 +9005,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", - "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz", + "integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==", "cpu": [ "riscv64" ], @@ -9025,9 +9025,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", - "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz", + "integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==", "cpu": [ "riscv64" ], @@ -9045,9 +9045,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", - "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz", + "integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==", "cpu": [ "s390x" ], @@ -9065,9 +9065,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", - "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz", + "integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==", "cpu": [ "x64" ], @@ -9085,9 +9085,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", - "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz", + "integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==", "cpu": [ "x64" ], @@ -9105,9 +9105,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", - "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz", + "integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==", "cpu": [ "arm64" ], @@ -9122,9 +9122,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", - "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz", + "integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==", "cpu": [ "arm64" ], @@ -9139,9 +9139,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", - "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz", + "integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==", "cpu": [ "ia32" ], @@ -9156,9 +9156,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", - "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz", + "integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==", "cpu": [ "x64" ], @@ -31942,9 +31942,9 @@ } }, "node_modules/oxlint": { - "version": "1.79.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", - "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz", + "integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==", "dev": true, "license": "MIT", "bin": { @@ -31957,25 +31957,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.79.0", - "@oxlint/binding-android-arm64": "1.79.0", - "@oxlint/binding-darwin-arm64": "1.79.0", - "@oxlint/binding-darwin-x64": "1.79.0", - "@oxlint/binding-freebsd-x64": "1.79.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", - "@oxlint/binding-linux-arm-musleabihf": "1.79.0", - "@oxlint/binding-linux-arm64-gnu": "1.79.0", - "@oxlint/binding-linux-arm64-musl": "1.79.0", - "@oxlint/binding-linux-ppc64-gnu": "1.79.0", - "@oxlint/binding-linux-riscv64-gnu": "1.79.0", - "@oxlint/binding-linux-riscv64-musl": "1.79.0", - "@oxlint/binding-linux-s390x-gnu": "1.79.0", - "@oxlint/binding-linux-x64-gnu": "1.79.0", - "@oxlint/binding-linux-x64-musl": "1.79.0", - "@oxlint/binding-openharmony-arm64": "1.79.0", - "@oxlint/binding-win32-arm64-msvc": "1.79.0", - "@oxlint/binding-win32-ia32-msvc": "1.79.0", - "@oxlint/binding-win32-x64-msvc": "1.79.0" + "@oxlint/binding-android-arm-eabi": "1.80.0", + "@oxlint/binding-android-arm64": "1.80.0", + "@oxlint/binding-darwin-arm64": "1.80.0", + "@oxlint/binding-darwin-x64": "1.80.0", + "@oxlint/binding-freebsd-x64": "1.80.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", + "@oxlint/binding-linux-arm-musleabihf": "1.80.0", + "@oxlint/binding-linux-arm64-gnu": "1.80.0", + "@oxlint/binding-linux-arm64-musl": "1.80.0", + "@oxlint/binding-linux-ppc64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-musl": "1.80.0", + "@oxlint/binding-linux-s390x-gnu": "1.80.0", + "@oxlint/binding-linux-x64-gnu": "1.80.0", + "@oxlint/binding-linux-x64-musl": "1.80.0", + "@oxlint/binding-openharmony-arm64": "1.80.0", + "@oxlint/binding-win32-arm64-msvc": "1.80.0", + "@oxlint/binding-win32-ia32-msvc": "1.80.0", + "@oxlint/binding-win32-x64-msvc": "1.80.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 5caad3aa328b..0556517054b1 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -332,7 +332,7 @@ "minimizer-webpack-plugin": "^5.6.1", "open-cli": "^9.0.0", "oxfmt": "^0.64.0", - "oxlint": "^1.79.0", + "oxlint": "^1.80.0", "po2json": "^0.4.5", "postcss-styled-syntax": "^0.7.2", "process": "^0.11.10", From 9656ecc3affc5e922cf6b7e4948b562072d3e234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:51:16 +0700 Subject: [PATCH 05/12] chore(deps-dev): bump @types/node from 26.2.0 to 26.3.0 in /superset-frontend (#43752) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 10 +++++----- superset-frontend/package.json | 2 +- .../packages/superset-ui-core/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index cc2b61d68bca..1fa3c7d1ccad 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -200,7 +200,7 @@ "@types/json-bigint": "^1.0.4", "@types/lodash-es": "^4.17.12", "@types/mousetrap": "^1.6.15", - "@types/node": "^26.2.0", + "@types/node": "^26.3.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@types/react-loadable": "^5.5.11", @@ -12788,9 +12788,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz", + "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -43024,7 +43024,7 @@ "@types/d3-time-format": "^4.0.3", "@types/jquery": "^4.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.2.0", + "@types/node": "^26.3.0", "@types/prop-types": "^15.7.15", "@types/react-syntax-highlighter": "^15.5.13", "@types/react-table": "^7.7.20", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 0556517054b1..630378ad6639 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -277,7 +277,7 @@ "@types/json-bigint": "^1.0.4", "@types/lodash-es": "^4.17.12", "@types/mousetrap": "^1.6.15", - "@types/node": "^26.2.0", + "@types/node": "^26.3.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@types/react-loadable": "^5.5.11", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index a048d31e6adb..37f5d6c3af46 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -103,7 +103,7 @@ "@types/d3-time-format": "^4.0.3", "@types/jquery": "^4.0.1", "@types/lodash": "^4.17.25", - "@types/node": "^26.2.0", + "@types/node": "^26.3.0", "@types/prop-types": "^15.7.15", "@types/react-syntax-highlighter": "^15.5.13", "@types/react-table": "^7.7.20", From 3b5a4baa0558241a1376b1f5ad44a8a586e6f05b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:53:55 +0700 Subject: [PATCH 06/12] chore(deps-dev): bump oxfmt from 0.64.0 to 0.65.0 in /superset-frontend (#43751) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 160 ++++++++++++++-------------- superset-frontend/package.json | 2 +- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 1fa3c7d1ccad..875ad5de4b8d 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -254,7 +254,7 @@ "mini-css-extract-plugin": "^2.10.2", "minimizer-webpack-plugin": "^5.6.1", "open-cli": "^9.0.0", - "oxfmt": "^0.64.0", + "oxfmt": "^0.65.0", "oxlint": "^1.80.0", "po2json": "^0.4.5", "postcss-styled-syntax": "^0.7.2", @@ -8479,9 +8479,9 @@ ] }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", - "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz", + "integrity": "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==", "cpu": [ "arm" ], @@ -8496,9 +8496,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", - "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz", + "integrity": "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==", "cpu": [ "arm64" ], @@ -8513,9 +8513,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", - "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz", + "integrity": "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==", "cpu": [ "arm64" ], @@ -8530,9 +8530,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", - "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz", + "integrity": "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==", "cpu": [ "x64" ], @@ -8547,9 +8547,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", - "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz", + "integrity": "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==", "cpu": [ "x64" ], @@ -8564,9 +8564,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", - "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz", + "integrity": "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==", "cpu": [ "arm" ], @@ -8581,9 +8581,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", - "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz", + "integrity": "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==", "cpu": [ "arm" ], @@ -8598,9 +8598,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", - "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz", + "integrity": "sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==", "cpu": [ "arm64" ], @@ -8618,9 +8618,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", - "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz", + "integrity": "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==", "cpu": [ "arm64" ], @@ -8638,9 +8638,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", - "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz", + "integrity": "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==", "cpu": [ "ppc64" ], @@ -8658,9 +8658,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", - "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz", + "integrity": "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==", "cpu": [ "riscv64" ], @@ -8678,9 +8678,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", - "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz", + "integrity": "sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==", "cpu": [ "riscv64" ], @@ -8698,9 +8698,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", - "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz", + "integrity": "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==", "cpu": [ "s390x" ], @@ -8718,9 +8718,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", - "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz", + "integrity": "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==", "cpu": [ "x64" ], @@ -8738,9 +8738,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", - "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz", + "integrity": "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==", "cpu": [ "x64" ], @@ -8758,9 +8758,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", - "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz", + "integrity": "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==", "cpu": [ "arm64" ], @@ -8775,9 +8775,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", - "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz", + "integrity": "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==", "cpu": [ "arm64" ], @@ -8792,9 +8792,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", - "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz", + "integrity": "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==", "cpu": [ "ia32" ], @@ -8809,9 +8809,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", - "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz", + "integrity": "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==", "cpu": [ "x64" ], @@ -31890,9 +31890,9 @@ } }, "node_modules/oxfmt": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", - "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.65.0.tgz", + "integrity": "sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==", "dev": true, "license": "MIT", "dependencies": { @@ -31908,25 +31908,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.64.0", - "@oxfmt/binding-android-arm64": "0.64.0", - "@oxfmt/binding-darwin-arm64": "0.64.0", - "@oxfmt/binding-darwin-x64": "0.64.0", - "@oxfmt/binding-freebsd-x64": "0.64.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", - "@oxfmt/binding-linux-arm64-gnu": "0.64.0", - "@oxfmt/binding-linux-arm64-musl": "0.64.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", - "@oxfmt/binding-linux-riscv64-musl": "0.64.0", - "@oxfmt/binding-linux-s390x-gnu": "0.64.0", - "@oxfmt/binding-linux-x64-gnu": "0.64.0", - "@oxfmt/binding-linux-x64-musl": "0.64.0", - "@oxfmt/binding-openharmony-arm64": "0.64.0", - "@oxfmt/binding-win32-arm64-msvc": "0.64.0", - "@oxfmt/binding-win32-ia32-msvc": "0.64.0", - "@oxfmt/binding-win32-x64-msvc": "0.64.0" + "@oxfmt/binding-android-arm-eabi": "0.65.0", + "@oxfmt/binding-android-arm64": "0.65.0", + "@oxfmt/binding-darwin-arm64": "0.65.0", + "@oxfmt/binding-darwin-x64": "0.65.0", + "@oxfmt/binding-freebsd-x64": "0.65.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.65.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.65.0", + "@oxfmt/binding-linux-arm64-gnu": "0.65.0", + "@oxfmt/binding-linux-arm64-musl": "0.65.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-musl": "0.65.0", + "@oxfmt/binding-linux-s390x-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-musl": "0.65.0", + "@oxfmt/binding-openharmony-arm64": "0.65.0", + "@oxfmt/binding-win32-arm64-msvc": "0.65.0", + "@oxfmt/binding-win32-ia32-msvc": "0.65.0", + "@oxfmt/binding-win32-x64-msvc": "0.65.0" }, "peerDependencies": { "svelte": "^5.0.0", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 630378ad6639..980e97691d0f 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -331,7 +331,7 @@ "mini-css-extract-plugin": "^2.10.2", "minimizer-webpack-plugin": "^5.6.1", "open-cli": "^9.0.0", - "oxfmt": "^0.64.0", + "oxfmt": "^0.65.0", "oxlint": "^1.80.0", "po2json": "^0.4.5", "postcss-styled-syntax": "^0.7.2", From 842efd5f120f15c9ecfd13bf7444bbc818bc4469 Mon Sep 17 00:00:00 2001 From: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:16:40 +0200 Subject: [PATCH 07/12] fix(datasets): warn on percent D3 format for count metrics (#43453) --- superset-frontend/package-lock.json | 3 +- superset-frontend/package.json | 1 - .../packages/superset-ui-core/package.json | 2 +- .../src/number-format/index.ts | 2 + .../DatasourceEditor/DatasourceEditor.tsx | 75 ++++++- ...tasourceEditorMetricFormatWarning.test.tsx | 210 ++++++++++++++++++ .../Datasource/components/Fieldset/index.tsx | 3 + .../src/setup/setupFormatters.ts | 2 +- superset-frontend/src/types/bootstrapTypes.ts | 2 +- 9 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 superset-frontend/src/components/Datasource/components/DatasourceEditor/tests/DatasourceEditorMetricFormatWarning.test.tsx diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 875ad5de4b8d..670e9571bdaa 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -69,7 +69,6 @@ "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", "@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", - "@types/d3-format": "^3.0.1", "@types/d3-selection": "^3.0.11", "@types/d3-time-format": "^4.0.3", "@types/react-google-recaptcha": "^2.1.9", @@ -42974,6 +42973,7 @@ "@apache-superset/core": "*", "@babel/runtime": "^7.29.7", "@braintree/sanitize-url": "^7.1.2", + "@types/d3-format": "^3.0.4", "@types/json-bigint": "^1.0.4", "@visx/responsive": "^4.0.0", "ace-builds": "^1.44.0", @@ -43017,7 +43017,6 @@ }, "devDependencies": { "@emotion/styled": "^11.14.1", - "@types/d3-format": "^3.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-scale": "^4.0.9", "@types/d3-time": "^3.0.4", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 980e97691d0f..7ec39fe22127 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -146,7 +146,6 @@ "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", "@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", - "@types/d3-format": "^3.0.1", "@types/d3-selection": "^3.0.11", "@types/d3-time-format": "^4.0.3", "@types/react-google-recaptcha": "^2.1.9", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 37f5d6c3af46..7c2330f572cc 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -53,6 +53,7 @@ "@apache-superset/core": "*", "@babel/runtime": "^7.29.7", "@braintree/sanitize-url": "^7.1.2", + "@types/d3-format": "^3.0.4", "@types/json-bigint": "^1.0.4", "@visx/responsive": "^4.0.0", "ace-builds": "^1.44.0", @@ -96,7 +97,6 @@ }, "devDependencies": { "@emotion/styled": "^11.14.1", - "@types/d3-format": "^3.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-scale": "^4.0.9", "@types/d3-time": "^3.0.4", diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts index d6d6096d8fcf..87b6633feb03 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts @@ -19,6 +19,8 @@ export { default as NumberFormats } from './NumberFormats'; export { default as NumberFormatter, PREVIEW_VALUE } from './NumberFormatter'; +export { formatSpecifier } from 'd3-format'; +export type { FormatLocaleDefinition } from 'd3-format'; export { DEFAULT_D3_FORMAT } from './D3FormatConfig'; export { diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx index a20e5e4b8d33..394806b17169 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx @@ -36,6 +36,7 @@ import { SupersetClient, getClientErrorObject, getExtensionsRegistry, + formatSpecifier, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/common'; import { t } from '@apache-superset/core/translation'; @@ -827,6 +828,78 @@ function EditorsSelector({ const ResultTable = extensionsRegistry.get('sqleditor.extension.resultTable') ?? FilterableTable; +// D3's '%' and 'p' types both multiply by 100; parsed via d3-format's own +// grammar so garbage like "foo%" is rejected rather than matched by suffix. +// The stored value is trimmed before parsing because +// NumberFormatterRegistry.get() trims it the same way before rendering, so +// this check agrees with what the renderer actually sees. +export const isPercentD3Format = (d3format?: string): boolean => { + if (!d3format) { + return false; + } + try { + const { type } = formatSpecifier(d3format.trim()); + return type === '%' || type === 'p'; + } catch { + return false; + } +}; + +// Matches the outermost COUNT(...) call's parens by depth, so a ratio like +// `COUNT(*) / COUNT(*)` isn't misclassified but a nested call like +// `COUNT(DISTINCT COALESCE(a, b))` is still recognized. Parens inside a +// quoted string literal (single- or double-quoted, with a doubled quote as +// an escaped quote) are ignored so they don't desync the depth count. +export const isCountExpression = (expression?: string): boolean => { + const trimmed = expression?.trim(); + if (!trimmed || !/^count\s*\(/i.test(trimmed) || !trimmed.endsWith(')')) { + return false; + } + let depth = 0; + let stringDelimiter: string | null = null; + for (let i = trimmed.indexOf('('); i < trimmed.length; i += 1) { + const char = trimmed[i]; + if (stringDelimiter) { + if (char === stringDelimiter && trimmed[i + 1] === stringDelimiter) { + i += 1; + } else if (char === stringDelimiter) { + stringDelimiter = null; + } + } else if (char === "'" || char === '"') { + stringDelimiter = char; + } else if (char === '(') { + depth += 1; + } else if (char === ')') { + depth -= 1; + if (depth === 0) { + return i === trimmed.length - 1; + } + } + } + return false; +}; + +function renderMetricFormatWarning(item: Record): ReactNode { + if ( + !isCountExpression(item.expression) || + !isPercentD3Format(item.d3format) + ) { + return null; + } + return ( + ({ marginBottom: themeParam.sizeUnit * 4 })} + type="warning" + showIcon + message={t( + 'This metric is a count, but its D3 format is a percentage. ' + + 'Percent formats multiply the value by 100, which will make a ' + + 'raw count render as a misleadingly large number.', + )} + /> + ); +} + // Redux connector types interface QueryPayload { client_id?: string; @@ -2170,7 +2243,7 @@ function DatasourceEditor({ }} expandFieldset={ -
+
{ + fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT }); + setupDatasourceEditorMocks(); +}); + +afterEach(async () => { + await cleanupAsyncOperations(); + fetchMock.clearHistory().removeRoutes(); +}); + +const WARNING_TEXT = /D3 format is a percentage/i; + +// Selecting the expand toggle by its position in the list is brittle: any +// change to the fixture or the table's sort order would expand a different +// row and negative assertions would keep passing against the wrong metric. +// Look up the toggle via the row that actually contains the metric name. +const expandMetricRow = async (metricName: string) => { + const nameCell = await screen.findByText(metricName); + const row = nameCell.closest('tr'); + if (!row) { + throw new Error(`Could not find a table row for metric "${metricName}"`); + } + await userEvent.click(within(row).getByLabelText(/expand row/i)); +}; + +// A fixed-time sleep can't prove the debounced value actually committed, so +// negative assertions would pass vacuously if the commit landed late on a +// loaded runner. Instead, wait for the real signal of a commit: the metric's +// d3format reaching the top-level onChange the editor calls after every +// datasource state update. +const waitForD3FormatCommit = ( + onChange: DatasourceEditorProps['onChange'], + metricName: string, + d3format: string, +) => + waitFor(() => { + const [datasource] = onChange.mock.calls.at(-1) ?? []; + const metric = datasource?.metrics?.find( + (m: { metric_name?: string }) => m.metric_name === metricName, + ); + expect(metric?.d3format).toBe(d3format); + }); + +test('isCountExpression matches a COUNT(...) call, including nested calls', () => { + expect(isCountExpression('COUNT(*)')).toBe(true); + expect(isCountExpression('count( * )')).toBe(true); + expect(isCountExpression('COUNT (*)')).toBe(true); + expect(isCountExpression('COUNT(DISTINCT name)')).toBe(true); + expect(isCountExpression('COUNT(DISTINCT COALESCE(a, b))')).toBe(true); + expect(isCountExpression('COUNT(*) / COUNT(*)')).toBe(false); + expect(isCountExpression('COUNT(*) * 100')).toBe(false); + expect(isCountExpression('SUM(num)')).toBe(false); + expect(isCountExpression(undefined)).toBe(false); +}); + +test('isCountExpression ignores parens inside string literals', () => { + expect(isCountExpression("COUNT(CASE WHEN x = '(' THEN 1 END)")).toBe(true); + expect(isCountExpression("COUNT(CASE WHEN x = ')' THEN 1 END)")).toBe(true); + expect(isCountExpression("COUNT(CASE WHEN x = '''(' THEN 1 END)")).toBe(true); +}); + +test('isCountExpression ignores parens inside double-quoted identifiers', () => { + expect(isCountExpression('COUNT("x\'")')).toBe(true); + expect(isCountExpression('COUNT("y\'")')).toBe(true); + expect(isCountExpression('COUNT(CASE WHEN "a""b" = 1 THEN 1 END)')).toBe( + true, + ); +}); + +test('isPercentD3Format accepts only a valid D3 percent/p spec', () => { + expect(isPercentD3Format('.0%')).toBe(true); + expect(isPercentD3Format(',.2%')).toBe(true); + expect(isPercentD3Format('.1p')).toBe(true); + expect(isPercentD3Format('foo%')).toBe(false); + expect(isPercentD3Format('.0%garbage%')).toBe(false); + expect(isPercentD3Format(',.0f')).toBe(false); + expect(isPercentD3Format(undefined)).toBe(false); +}); + +// NumberFormatterRegistry.get() trims the stored value before parsing it at +// render time, so this must trim too rather than reject a format the +// renderer accepts. +test('isPercentD3Format trims, matching render-time parsing', () => { + expect(isPercentD3Format('.0% ')).toBe(true); +}); + +// A '%' format is valid syntax, so it never hits the "Invalid format" fallback. +test('warns when a percent D3 format is set on a COUNT metric', async () => { + const testProps = createProps(); + fastRender(testProps); + await dismissDatasourceWarning(); + + await userEvent.click(await screen.findByTestId('collection-tab-Metrics')); + await expandMetricRow('count'); + + expect(screen.queryByText(WARNING_TEXT)).not.toBeInTheDocument(); + + await userEvent.type(await screen.findByPlaceholderText('%y/%m/%d'), '.0%'); + + expect(await screen.findByText(WARNING_TEXT)).toBeInTheDocument(); +}); + +test('does not warn for a non-percent format on a COUNT metric', async () => { + const testProps = createProps(); + fastRender(testProps); + await dismissDatasourceWarning(); + + await userEvent.click(await screen.findByTestId('collection-tab-Metrics')); + await expandMetricRow('count'); + + await userEvent.type(await screen.findByPlaceholderText('%y/%m/%d'), ',.0f'); + + await waitForD3FormatCommit(testProps.onChange, 'count', ',.0f'); + expect(screen.queryByText(WARNING_TEXT)).not.toBeInTheDocument(); +}); + +test('does not warn for a percent format on a non-COUNT metric', async () => { + const testProps = createProps(); + fastRender(testProps); + await dismissDatasourceWarning(); + + await userEvent.click(await screen.findByTestId('collection-tab-Metrics')); + await expandMetricRow('sum__num'); + + await userEvent.type(await screen.findByPlaceholderText('%y/%m/%d'), '.0%'); + + await waitForD3FormatCommit(testProps.onChange, 'sum__num', '.0%'); + expect(screen.queryByText(WARNING_TEXT)).not.toBeInTheDocument(); +}); + +test('does not warn for a ratio built from COUNT, e.g. COUNT(*) / COUNT(*)', async () => { + const baseProps = createProps(); + const testProps = { + ...baseProps, + datasource: { + ...baseProps.datasource, + metrics: [ + ...baseProps.datasource.metrics, + { + id: 99, + uuid: 'metric-99-uuid', + expression: 'COUNT(*) / COUNT(*)', + verbose_name: 'ratio', + metric_name: 'ratio', + metric_type: 'count', + }, + ], + }, + }; + fastRender(testProps); + await dismissDatasourceWarning(); + + await userEvent.click(await screen.findByTestId('collection-tab-Metrics')); + await expandMetricRow('ratio'); + + await userEvent.type(await screen.findByPlaceholderText('%y/%m/%d'), '.0%'); + + await waitForD3FormatCommit(testProps.onChange, 'ratio', '.0%'); + expect(screen.queryByText(WARNING_TEXT)).not.toBeInTheDocument(); +}); + +test('does not warn for a garbage format string that merely ends in %', async () => { + const testProps = createProps(); + fastRender(testProps); + await dismissDatasourceWarning(); + + await userEvent.click(await screen.findByTestId('collection-tab-Metrics')); + await expandMetricRow('count'); + + await userEvent.type(await screen.findByPlaceholderText('%y/%m/%d'), 'foo%'); + + await waitForD3FormatCommit(testProps.onChange, 'count', 'foo%'); + expect(screen.queryByText(WARNING_TEXT)).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/Datasource/components/Fieldset/index.tsx b/superset-frontend/src/components/Datasource/components/Fieldset/index.tsx index e74d25f69655..e2f77be2c47e 100644 --- a/superset-frontend/src/components/Datasource/components/Fieldset/index.tsx +++ b/superset-frontend/src/components/Datasource/components/Fieldset/index.tsx @@ -28,6 +28,7 @@ export interface FieldsetProps { item?: Record; title?: ReactNode; compact?: boolean; + renderWarning?: (item: Record) => ReactNode; } type fieldKeyType = string | number; @@ -38,6 +39,7 @@ export default function Fieldset({ item = {}, title = null, compact = false, + renderWarning, }: FieldsetProps) { // Controls report their edits asynchronously - TextControl debounces by // FAST_DEBOUNCE - so the callback that eventually fires was built during an @@ -78,6 +80,7 @@ export default function Fieldset({ )} + {renderWarning?.(item)} {recurseReactClone(children, Field, propExtender)} ); diff --git a/superset-frontend/src/setup/setupFormatters.ts b/superset-frontend/src/setup/setupFormatters.ts index a96074f71a84..d041913a3bb7 100644 --- a/superset-frontend/src/setup/setupFormatters.ts +++ b/superset-frontend/src/setup/setupFormatters.ts @@ -32,8 +32,8 @@ import { setCurrencyLocale, createLengthFormatter, createThroughputFormatter, + FormatLocaleDefinition, } from '@superset-ui/core'; -import { FormatLocaleDefinition } from 'd3-format'; import { TimeLocaleDefinition } from 'd3-time-format'; export default function setupFormatters( diff --git a/superset-frontend/src/types/bootstrapTypes.ts b/superset-frontend/src/types/bootstrapTypes.ts index 9a10e88d684b..9c341034f382 100644 --- a/superset-frontend/src/types/bootstrapTypes.ts +++ b/superset-frontend/src/types/bootstrapTypes.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { FormatLocaleDefinition } from 'd3-format'; import { TimeLocaleDefinition } from 'd3-time-format'; import { isPlainObject } from 'lodash-es'; import { Languages } from 'src/features/home/LanguagePicker'; @@ -28,6 +27,7 @@ import { import type { ColorSchemeConfig, FeatureFlagMap, + FormatLocaleDefinition, JsonObject, SequentialSchemeConfig, } from '@superset-ui/core'; From 439564041f54cafbc17f2f995c682c65b8bbbaab Mon Sep 17 00:00:00 2001 From: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:17:11 +0200 Subject: [PATCH 08/12] fix(echarts): place weekly time-axis ticks on the data buckets (#43339) --- .../src/MixedTimeseries/transformProps.ts | 42 ++- .../src/Timeseries/transformProps.ts | 57 ++-- .../plugin-chart-echarts/src/constants.ts | 10 + .../plugin-chart-echarts/src/utils/series.ts | 164 ++++++++++ .../MixedTimeseries/transformProps.test.ts | 92 ++++++ .../test/Timeseries/transformProps.test.ts | 292 ++++++++++++++++++ .../test/utils/series.test.ts | 145 +++++++++ 7 files changed, 756 insertions(+), 46 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 8a9bae65b6e5..10c8014c97af 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -73,6 +73,8 @@ import { getLegendProps, getMinAndMaxFromBounds, getOverMaxHiddenFormatter, + getTemporalAxisTickConfig, + resolveTemporalTickValues, } from '../utils/series'; import { resolveLegendLayout } from '../utils/legendLayout'; import { @@ -764,6 +766,26 @@ export default function transformProps( const { setDataMask = () => {}, onContextMenu } = hooks; const alignTicks = yAxisIndex !== yAxisIndexB; + // Both queries share the axis, so a bucket contributed by either needs a tick. + const temporalTickValues = resolveTemporalTickValues( + [...rebasedDataA, ...rebasedDataB], + xAxisLabel, + xAxisType, + resolvedTimeGrain, + annotationLayers, + ); + + const temporalAxisTickConfig = getTemporalAxisTickConfig( + temporalTickValues, + showMaxLabel, + xAxisType, + xAxisLabelRotation, + xAxisLabelInterval, + deduplicatedFormatter, + false, + zoomable, + ); + const echartOptions: EChartsCoreOption = { useUTC: true, grid: { @@ -775,22 +797,12 @@ export default function transformProps( name: xAxisTitle, nameGap: xAxisTitleMarginPx, nameLocation: 'middle', - axisLabel: { - hideOverlap: showMaxLabel - ? false - : !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0), - formatter: deduplicatedFormatter, - rotate: xAxisLabelRotation, - interval: xAxisLabelInterval, - ...(showMaxLabel && { - showMaxLabel: true, - alignMaxLabel: 'right', - showMinLabel: true, - alignMinLabel: 'left', - }), - }, + ...temporalAxisTickConfig, minorTick: { show: minorTicks }, - axisTick: { show: axisTicks ? 'auto' : false }, + axisTick: { + ...temporalAxisTickConfig.axisTick, + show: axisTicks ? 'auto' : false, + }, ...(gridlines ? {} : { splitLine: { show: false } }), minInterval: xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index a67d373f2c91..427847854e1c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -88,6 +88,8 @@ import { getHorizontalLegendAvailableWidth, getLegendProps, getMinAndMaxFromBounds, + getTemporalAxisTickConfig, + resolveTemporalTickValues, } from '../utils/series'; import { resolveLegendLayout } from '../utils/legendLayout'; import { @@ -1247,6 +1249,25 @@ export default function transformProps( })() : xAxisFormatter; + const temporalTickValues = resolveTemporalTickValues( + rebasedData, + xAxisLabel, + xAxisType, + resolvedTimeGrain, + annotationLayers, + ); + + const temporalAxisTickConfig = getTemporalAxisTickConfig( + temporalTickValues, + showMaxLabel, + xAxisType, + xAxisLabelRotation, + xAxisLabelInterval, + deduplicatedFormatter, + isHorizontal, + zoomable, + ); + let xAxis: any = { type: xAxisType, name: xAxisTitle, @@ -1256,38 +1277,12 @@ export default function transformProps( groupBy.length === 0 && { triggerEvent: true, }), - axisLabel: { - // When rotation is applied on time axes, hideOverlap can - // aggressively hide the last label. Rotated labels already - // have less overlap, so disabling hideOverlap is safe. - // At 0° rotation, also disable hideOverlap when showMaxLabel - // is active so the forced boundary label is never suppressed - // by ECharts' overlap detection (#39899). - hideOverlap: showMaxLabel - ? false - : !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0), - formatter: deduplicatedFormatter, - rotate: xAxisLabelRotation, - interval: xAxisLabelInterval, - // Force the boundary labels on non-rotated time axes so the first - // and last dates stay visible: hideOverlap can hide the last label, - // and a min date that falls between "nice" ticks otherwise renders - // no beginning label. Skipped when rotated to avoid phantom labels - // at the axis boundary. - ...(showMaxLabel && { - showMaxLabel: true, - showMinLabel: true, - }), - // The alignments assume the axis runs along the bottom; a horizontal - // chart puts this axis on the side, where they misplace the labels. - ...(showMaxLabel && - !isHorizontal && { - alignMaxLabel: 'right', - alignMinLabel: 'left', - }), - }, + ...temporalAxisTickConfig, minorTick: { show: minorTicks }, - axisTick: { show: axisTicks ? 'auto' : false }, + axisTick: { + ...temporalAxisTickConfig.axisTick, + show: axisTicks ? 'auto' : false, + }, ...(gridlines ? {} : { splitLine: { show: false } }), minInterval: xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index 3044bf10d704..2b94967ae003 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -89,6 +89,16 @@ export const StackControlOptionsWithoutStream: [ [StackControlsValue.Stack, t('Stack')], ]; +// Grains ECharts' time axis cannot tick on; see getTemporalTickValues in +// utils/series. +export const WEEKLY_TIME_GRAINS: ReadonlySet = new Set([ + TimeGranularity.WEEK, + TimeGranularity.WEEK_STARTING_SUNDAY, + TimeGranularity.WEEK_STARTING_MONDAY, + TimeGranularity.WEEK_ENDING_SATURDAY, + TimeGranularity.WEEK_ENDING_SUNDAY, +]); + export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.HOUR]: 3600 * 1000, [TimeGranularity.DAY]: 3600 * 1000 * 24, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index cdfaaf78c185..6ededf12ac3a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -18,12 +18,14 @@ * under the License. */ import { + AnnotationLayer, AxisType, ChartDataResponseResult, DataRecord, DataRecordValue, DTTM_ALIAS, ensureIsArray, + isTimeseriesAnnotationLayer, LegendState, normalizeTimestamp, NumberFormats, @@ -42,6 +44,7 @@ import { NULL_STRING, StackControlsValue, TIMESERIES_CONSTANTS, + WEEKLY_TIME_GRAINS, } from '../constants'; import { EchartsTimeseriesSeriesType, @@ -986,6 +989,167 @@ export function getAxisType( return AxisType.Category; } +// `new Date('2024-04-06')` parses as UTC, but ECharts' own date parser treats +// zone-less strings as local time — mismatch would offset the pinned tick. +const DATE_ONLY_RE = /^(\d{4})(?:-(\d{1,2})(?:-(\d{1,2}))?)?$/; + +function parseTemporalString(value: string): number { + const dateOnly = DATE_ONLY_RE.exec(value); + if (dateOnly) { + const [, year, month, day] = dateOnly; + return new Date( + Number(year), + Number(month || 1) - 1, + Number(day || 1), + ).getTime(); + } + return new Date(value).getTime(); +} + +/** + * Bucket timestamps a temporal axis should tick on, or undefined to let ECharts + * choose. + * + * ECharts generates time ticks from a calendar ladder with no week unit, so for + * weekly data it steps days from the 1st of each month instead: labels drift + * across weekdays and snap to month starts (#17226). Coarser grains already land + * on their data and keep ECharts' calendar-nice labels. + */ +export function getTemporalTickValues( + data: DataRecord[], + xAxisLabel: string, + xAxisType: AxisType, + timeGrain?: string, +): number[] | undefined { + if ( + xAxisType !== AxisType.Time || + !timeGrain || + !WEEKLY_TIME_GRAINS.has(timeGrain) + ) { + return undefined; + } + const values = new Set(); + data.forEach(row => { + const value = row[xAxisLabel]; + const timestamp = + // eslint-disable-next-line no-nested-ternary + value instanceof Date + ? value.getTime() + : typeof value === 'string' + ? parseTemporalString(value) + : Number(value ?? NaN); + if (Number.isFinite(timestamp)) { + values.add(timestamp); + } + }); + return values.size ? [...values].sort((a, b) => a - b) : undefined; +} + +/** + * Weekly grains: pin the ticks to the buckets ECharts would otherwise miss. + * A timeseries annotation contributes its own timestamps and widens the axis + * past the buckets, and ECharts clips pinned ticks to the extent, so that + * span would render bare — leave those charts on ECharts' own ticks. + */ +export function resolveTemporalTickValues( + data: DataRecord[], + xAxisLabel: string, + xAxisType: AxisType, + timeGrain: string | undefined, + annotationLayers: AnnotationLayer[], +): number[] | undefined { + const hasTimeseriesAnnotation = annotationLayers.some( + layer => layer.show && isTimeseriesAnnotationLayer(layer), + ); + return hasTimeseriesAnnotation + ? undefined + : getTemporalTickValues(data, xAxisLabel, xAxisType, timeGrain); +} + +// Unlike axisLabel, axisTick has no overlap-based thinning, so pinning it to +// every bucket combs a long weekly range. Downsample evenly, keeping ends. +const MAX_PINNED_AXIS_TICKS = 60; + +export function capTickMarks( + values: number[], + maxTicks: number = MAX_PINNED_AXIS_TICKS, +): number[] { + if (values.length <= maxTicks) { + return values; + } + const step = Math.ceil(values.length / maxTicks); + const capped = values.filter((_, index) => index % step === 0); + const last = values[values.length - 1]; + if (capped[capped.length - 1] !== last) { + capped.push(last); + } + return capped; +} + +/** + * axisLabel/axisTick fragment for a temporal x-axis, shared by Timeseries and + * MixedTimeseries. When temporalTickValues pins the axis to weekly buckets, + * axisTick.customValues (what splitLine/gridlines follow) is downsampled to + * avoid combing a long weekly range. axisLabel.customValues (what hideOverlap + * thins from) uses the same capped set on a non-zoomable axis, so a label + * surviving hideOverlap thinning always lands on a real tick and gridline + * rather than a capped-away bucket. On a zoomable axis the full set is used + * instead — zooming lets the user reach any bucket, but customValues never + * recomputes on dataZoom, so a capped set there would freeze the visible + * labels to the pre-zoom subset. + */ +export function getTemporalAxisTickConfig( + temporalTickValues: number[] | undefined, + showMaxLabel: boolean, + xAxisType: AxisType, + xAxisLabelRotation: number, + xAxisLabelInterval: number | string | undefined, + formatter: unknown, + isHorizontal: boolean = false, + zoomable: boolean = false, +): { + axisLabel: Record; + axisTick?: { customValues: number[] }; +} { + const cappedTickValues = temporalTickValues + ? capTickMarks(temporalTickValues) + : undefined; + const labelCustomValues = zoomable ? temporalTickValues : cappedTickValues; + return { + axisLabel: { + // Pinned ticks label every bucket, which does crowd, so thinning + // always wins there. + hideOverlap: + !!temporalTickValues || + (showMaxLabel + ? false + : !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0)), + formatter, + rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, + // Force the boundary labels so the first and last dates stay visible: + // hideOverlap can hide the last label, and a min date that falls + // between "nice" ticks otherwise renders no beginning label. Applied + // for pinned axes too — showMaxLabel only shields its immediate + // neighbour, so a farther label on a crowded weekly axis can still be + // dropped, but that's strictly better than no shielding at all. + ...(showMaxLabel && { + showMaxLabel: true, + showMinLabel: true, + }), + // The alignments assume the axis runs along the bottom; a horizontal + // chart puts this axis on the side, where they misplace the labels. + ...(showMaxLabel && + !isHorizontal && { + alignMaxLabel: 'right', + alignMinLabel: 'left', + }), + ...(labelCustomValues && { customValues: labelCustomValues }), + }, + ...(cappedTickValues && { axisTick: { customValues: cappedTickValues } }), + }; +} + export function getOverMaxHiddenFormatter( config: { max?: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts index d2a158a6e068..4004babdbcba 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts @@ -1512,6 +1512,98 @@ describe('EchartsMixedTimeseries tooltip truncation', () => { }); }); +describe('weekly x-axis tick alignment', () => { + const WEEK_MS = 7 * 24 * 3600 * 1000; + const MONDAYS = Array.from( + { length: 6 }, + (_, i) => Date.UTC(2026, 3, 6) + i * WEEK_MS, + ); + const weeklyLabelMap = { ds: ['ds'], sum__num: ['sum__num'] }; + + const weeklyQuery = (timestamps: number[]) => + createTestQueryData( + timestamps.map((ds, i) => ({ ds, sum__num: 10 + i })), + { + label_map: weeklyLabelMap, + colnames: ['ds', 'sum__num'], + coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], + }, + ); + + const weeklyChartProps = ( + queryA: number[], + queryB: number[], + overrides: Partial = {}, + ) => + createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: [weeklyQuery(queryA), weeklyQuery(queryB)], + formData: { + ...formData, + groupby: [], + groupbyB: [], + timeGrainSqla: TimeGranularity.WEEK_STARTING_MONDAY, + ...overrides, + }, + queriesData: [weeklyQuery(queryA), weeklyQuery(queryB)], + }); + + test('pins ticks, labels and gridlines to the weekly buckets', () => { + const { xAxis } = transformProps(weeklyChartProps(MONDAYS, MONDAYS)) + .echartOptions as any; + + expect(xAxis.type).toBe(AxisType.Time); + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + // Gridlines follow axisTick.customValues, so splitLine needs no own copy. + expect(xAxis.axisTick.customValues).toEqual(MONDAYS); + expect(xAxis.splitLine).toBeUndefined(); + }); + + test('keeps label thinning on when the labels are rotated', () => { + const { xAxis } = transformProps( + weeklyChartProps(MONDAYS, MONDAYS, { xAxisLabelRotation: 45 }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + expect(xAxis.axisLabel.hideOverlap).toBe(true); + }); + + test('keeps the showMaxLabel override at 0° rotation on pinned axes', () => { + // hideOverlap stays on for pinned ticks (they label every bucket), but + // showMaxLabel still shields the boundary label's immediate neighbour + // so the last bucket isn't silently dropped (#39899). + const { xAxis } = transformProps(weeklyChartProps(MONDAYS, MONDAYS)) + .echartOptions as any; + + expect(xAxis.axisLabel.showMaxLabel).toBe(true); + expect(xAxis.axisLabel.hideOverlap).toBe(true); + }); + + test('covers buckets contributed by either query', () => { + // The two queries share one axis, so a bucket present in only one of them + // still needs a tick. + const { xAxis } = transformProps( + weeklyChartProps(MONDAYS.slice(0, 3), MONDAYS.slice(2)), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + }); + + test('leaves grains ECharts places correctly untouched', () => { + const { xAxis } = transformProps( + weeklyChartProps(MONDAYS, MONDAYS, { + timeGrainSqla: TimeGranularity.MONTH, + }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toBeUndefined(); + expect(xAxis.axisTick?.customValues).toBeUndefined(); + }); +}); + function transformWithChrome( overrides: Partial, ) { diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts index 7c05d4f91d76..a24ff415857d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts @@ -17,6 +17,7 @@ * under the License. */ import { + AnnotationData, AnnotationSourceType, AnnotationStyle, AnnotationType, @@ -2706,6 +2707,297 @@ describe('EchartsTimeseries tooltip truncation', () => { }); }); +describe('weekly x-axis tick alignment', () => { + // 13 Monday-aligned weekly buckets, the shape produced by a dataset that is + // pre-aggregated to weeks. + const WEEK_MS = 7 * 24 * 3600 * 1000; + const MONDAYS = Array.from( + { length: 13 }, + (_, i) => Date.UTC(2026, 3, 6) + i * WEEK_MS, + ); + + const weeklyChartProps = ( + formDataOverrides: Partial = {}, + annotationData?: AnnotationData, + ) => + createTestChartProps({ + annotationData, + formData: { + granularity_sqla: 'ds', + timeGrainSqla: TimeGranularity.WEEK_STARTING_MONDAY, + xAxisTimeFormat: '%m-%d', + ...formDataOverrides, + }, + queriesData: [ + createTestQueryData( + MONDAYS.map((__timestamp, i) => ({ __timestamp, sales: 100 + i })), + { + colnames: ['__timestamp', 'sales'], + coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], + // transformProps reads annotations off the query, not chartProps. + ...(annotationData && { annotation_data: annotationData }), + }, + ), + ], + }); + + test('pins ticks, labels and gridlines to the weekly buckets', () => { + const { xAxis } = transformProps(weeklyChartProps()).echartOptions as any; + + expect(xAxis.type).toBe(AxisType.Time); + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + // Gridlines follow axisTick.customValues, so splitLine needs no own copy. + expect(xAxis.axisTick.customValues).toEqual(MONDAYS); + expect(xAxis.splitLine).toBeUndefined(); + }); + + const manyMondaysChartProps = (overrides: Record = {}) => { + const manyMondays = Array.from( + { length: 261 }, + (_, i) => Date.UTC(2021, 0, 4) + i * WEEK_MS, + ); + return { + manyMondays, + chartProps: createTestChartProps({ + formData: { + granularity_sqla: 'ds', + timeGrainSqla: TimeGranularity.WEEK_STARTING_MONDAY, + xAxisTimeFormat: '%m-%d', + ...overrides, + }, + queriesData: [ + createTestQueryData( + manyMondays.map((__timestamp, i) => ({ + __timestamp, + sales: 100 + i, + })), + { + colnames: ['__timestamp', 'sales'], + coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], + }, + ), + ], + }), + }; + }; + + test('caps both axisTick and axisLabel customValues on a non-zoomable axis', () => { + // customValues never recomputes, so on a non-zoomable axis (no dataZoom + // to reach hidden buckets) axisLabel is capped to the same subset as + // axisTick: a label surviving hideOverlap thinning then always lands on + // a real tick and gridline rather than a capped-away bucket. + const { manyMondays, chartProps } = manyMondaysChartProps(); + const { xAxis } = transformProps(chartProps).echartOptions as any; + + expect(xAxis.axisTick.customValues.length).toBeLessThan(manyMondays.length); + expect(xAxis.axisLabel.customValues).toEqual(xAxis.axisTick.customValues); + }); + + test('keeps the full bucket set for axisLabel on a zoomable axis', () => { + // A capped, uncapped label set would freeze the visible labels to the + // pre-zoom subset since customValues never recomputes on dataZoom, so a + // zoomable axis keeps the full set for axisLabel and lets hideOverlap + // thin it dynamically; only axisTick (no such thinning) stays capped. + const { manyMondays, chartProps } = manyMondaysChartProps({ + zoomable: true, + }); + const { xAxis } = transformProps(chartProps).echartOptions as any; + + expect(xAxis.axisTick.customValues.length).toBeLessThan(manyMondays.length); + expect(xAxis.axisLabel.customValues).toEqual(manyMondays); + }); + + test('keeps the showMaxLabel override at 0° rotation on pinned axes', () => { + // hideOverlap stays on for pinned ticks (they label every bucket), but + // showMaxLabel still shields the boundary label's immediate neighbour + // so the last bucket isn't silently dropped (#39899). + const { xAxis } = transformProps(weeklyChartProps()).echartOptions as any; + + expect(xAxis.axisLabel.showMaxLabel).toBe(true); + expect(xAxis.axisLabel.hideOverlap).toBe(true); + }); + + test('pins ticks when the bucket column holds ISO date strings', () => { + // A dataset can arrive with __timestamp serialized as an ISO string + // rather than a Date/epoch-ms value. + const chartProps = createTestChartProps({ + formData: { + granularity_sqla: 'ds', + timeGrainSqla: TimeGranularity.WEEK_STARTING_MONDAY, + }, + queriesData: [ + createTestQueryData( + MONDAYS.map((__timestamp, i) => ({ + __timestamp: new Date(__timestamp).toISOString(), + sales: 100 + i, + })), + { + colnames: ['__timestamp', 'sales'], + coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], + }, + ), + ], + }); + const { xAxis } = transformProps(chartProps).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + }); + + test('keeps label thinning on when the labels are rotated', () => { + // Rotation normally turns hideOverlap off, but pinned ticks put a label on + // every bucket, so without thinning a multi-year range draws hundreds. + const { xAxis } = transformProps( + weeklyChartProps({ xAxisLabelRotation: 45 }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + expect(xAxis.axisLabel.hideOverlap).toBe(true); + }); + + test('leaves rotation thinning alone when the ticks are not pinned', () => { + const { xAxis } = transformProps( + weeklyChartProps({ + timeGrainSqla: TimeGranularity.MONTH, + xAxisLabelRotation: 45, + }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toBeUndefined(); + expect(xAxis.axisLabel.hideOverlap).toBe(false); + }); + + const timeseriesLayer = (show: boolean) => + ({ + name: 'my annotation', + annotationType: AnnotationType.Timeseries, + sourceType: AnnotationSourceType.Line, + style: AnnotationStyle.Solid, + show, + value: 1, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any; + + // The annotation's own timestamps run a year past the last bucket. + const annotationRecords = { + 'my annotation': { + records: [ + { ds: MONDAYS[0], y: 1 }, + { ds: MONDAYS[12] + 52 * WEEK_MS, y: 2 }, + ], + }, + }; + + test('does not pin ticks when a timeseries annotation widens the axis', () => { + // A Time axis takes no min/max, so it stretches to cover the annotation + // while ECharts clips pinned ticks to the extent — that span would be bare. + const { xAxis } = transformProps( + weeklyChartProps( + { annotationLayers: [timeseriesLayer(true)] }, + annotationRecords, + ), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toBeUndefined(); + expect(xAxis.axisTick?.customValues).toBeUndefined(); + }); + + test('still pins ticks for a hidden timeseries annotation', () => { + const { xAxis } = transformProps( + weeklyChartProps( + { annotationLayers: [timeseriesLayer(false)] }, + annotationRecords, + ), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + }); + + test.each([ + TimeGranularity.WEEK, + TimeGranularity.WEEK_STARTING_SUNDAY, + TimeGranularity.WEEK_STARTING_MONDAY, + TimeGranularity.WEEK_ENDING_SATURDAY, + TimeGranularity.WEEK_ENDING_SUNDAY, + ])('applies to the %s grain', grain => { + const { xAxis } = transformProps(weeklyChartProps({ timeGrainSqla: grain })) + .echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + }); + + test('a dashboard time-grain override drives the alignment', () => { + const { xAxis } = transformProps( + weeklyChartProps({ + timeGrainSqla: TimeGranularity.DAY, + extraFormData: { time_grain_sqla: TimeGranularity.WEEK }, + }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual(MONDAYS); + }); + + test('deduplicates and sorts the bucket timestamps', () => { + // A grouped query repeats each bucket once per series, and the rows are + // not necessarily ordered. + const chartProps = createTestChartProps({ + formData: { + granularity_sqla: 'ds', + timeGrainSqla: TimeGranularity.WEEK, + groupby: ['region'], + }, + queriesData: [ + createTestQueryData( + [ + { __timestamp: MONDAYS[1], region: 'b', sales: 2 }, + { __timestamp: MONDAYS[0], region: 'a', sales: 1 }, + { __timestamp: MONDAYS[1], region: 'a', sales: 3 }, + { __timestamp: MONDAYS[0], region: 'b', sales: 4 }, + ], + { + colnames: ['__timestamp', 'region', 'sales'], + coltypes: [ + GenericDataType.Temporal, + GenericDataType.String, + GenericDataType.Numeric, + ], + }, + ), + ], + }); + const { xAxis } = transformProps(chartProps).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toEqual([MONDAYS[0], MONDAYS[1]]); + }); + + test('leaves grains ECharts places correctly untouched', () => { + ( + [ + TimeGranularity.DAY, + TimeGranularity.MONTH, + TimeGranularity.QUARTER, + TimeGranularity.YEAR, + undefined, + ] as const + ).forEach(grain => { + const { xAxis } = transformProps( + weeklyChartProps({ timeGrainSqla: grain }), + ).echartOptions as any; + + expect(xAxis.axisLabel.customValues).toBeUndefined(); + expect(xAxis.axisTick?.customValues).toBeUndefined(); + }); + }); + + test('leaves a categorical x-axis untouched', () => { + const { xAxis } = transformProps( + weeklyChartProps({ xAxisForceCategorical: true }), + ).echartOptions as any; + + expect(xAxis.type).toBe(AxisType.Category); + expect(xAxis.axisLabel.customValues).toBeUndefined(); + }); +}); + describe('tooltip for metrics whose labels end in forecast suffixes', () => { const marker = ''; const seriesIds = ['ci__yhat', 'ci__yhat_lower', 'ci__yhat_upper']; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index 32f16acfe851..78e88961a008 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -22,6 +22,7 @@ import { DataRecord, getNumberFormatter, getTimeFormatter, + TimeGranularity, } from '@superset-ui/core'; import { supersetTheme as theme } from '@apache-superset/core/theme'; import { GenericDataType } from '@apache-superset/core/common'; @@ -40,6 +41,8 @@ import { getLegendProps, getOverMaxHiddenFormatter, getMinAndMaxFromBounds, + capTickMarks, + getTemporalTickValues, sanitizeHtml, sortAndFilterSeries, sortRows, @@ -1705,6 +1708,148 @@ test('getAxisType does not coerce Numeric x-axis to Time regardless of values', ); }); +describe('getTemporalTickValues', () => { + const xAxisLabel = '__timestamp'; + + test('returns undefined for a non-time axis', () => { + const data: DataRecord[] = [{ [xAxisLabel]: 1712361600000 }]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Category, + TimeGranularity.WEEK, + ), + ).toBeUndefined(); + }); + + test('returns undefined when there is no time grain', () => { + const data: DataRecord[] = [{ [xAxisLabel]: 1712361600000 }]; + expect( + getTemporalTickValues(data, xAxisLabel, AxisType.Time, undefined), + ).toBeUndefined(); + }); + + test('returns undefined for a non-weekly time grain', () => { + const data: DataRecord[] = [{ [xAxisLabel]: 1712361600000 }]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.MONTH, + ), + ).toBeUndefined(); + }); + + test('returns sorted, de-duplicated bucket timestamps for numbers and Dates', () => { + const t0 = Date.UTC(2026, 3, 6); + const t1 = Date.UTC(2026, 3, 13); + const data: DataRecord[] = [ + { [xAxisLabel]: t1 }, + { [xAxisLabel]: new Date(t0) }, + { [xAxisLabel]: t0 }, // duplicate of the Date row above + ]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.WEEK, + ), + ).toEqual([t0, t1]); + }); + + test('parses a zoned ISO string as the instant it names', () => { + const data: DataRecord[] = [{ [xAxisLabel]: '2026-04-06T00:00:00.000Z' }]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.WEEK, + ), + ).toEqual([Date.UTC(2026, 3, 6)]); + }); + + test('parses a zone-less datetime string as local time, matching ECharts', () => { + const data: DataRecord[] = [{ [xAxisLabel]: '2026-04-06T00:00:00' }]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.WEEK, + ), + ).toEqual([new Date(2026, 3, 6, 0, 0, 0).getTime()]); + }); + + test('parses a bare date string as local midnight, matching ECharts rather than native Date', () => { + // `new Date('2026-04-06')` is UTC, but ECharts parses it as local time. + // jest.config.js fixes the test TZ to America/New_York, so they disagree. + const data: DataRecord[] = [{ [xAxisLabel]: '2026-04-06' }]; + const localMidnight = new Date(2026, 3, 6).getTime(); + expect(localMidnight).not.toEqual(new Date('2026-04-06').getTime()); + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.WEEK, + ), + ).toEqual([localMidnight]); + }); + + test('drops unparseable or nullish values and returns undefined when none remain', () => { + const data: DataRecord[] = [ + { [xAxisLabel]: 'not-a-date' }, + { [xAxisLabel]: null }, + ]; + expect( + getTemporalTickValues( + data, + xAxisLabel, + AxisType.Time, + TimeGranularity.WEEK, + ), + ).toBeUndefined(); + }); +}); + +describe('capTickMarks', () => { + test('returns values unchanged when within the cap', () => { + const values = [1, 2, 3]; + expect(capTickMarks(values, 60)).toEqual(values); + }); + + test('downsamples to every step-th value when the last value already lands on the step', () => { + const values = Array.from({ length: 261 }, (_, i) => i); + // step = ceil(261 / 60) = 5, and 260 is already a multiple of 5, so + // nothing needs to be appended for the last bucket. + expect(capTickMarks(values, 60)).toEqual( + Array.from({ length: 53 }, (_, i) => i * 5), + ); + }); + + test('appends the last value when it does not land on the step', () => { + const values = Array.from({ length: 262 }, (_, i) => i); + // step = ceil(262 / 60) = 5, stepping lands on 0..260, and the true last + // value (261) is appended on top since it isn't a multiple of 5. + expect(capTickMarks(values, 60)).toEqual([ + ...Array.from({ length: 53 }, (_, i) => i * 5), + 261, + ]); + }); + + test('maxTicks is not a hard bound once the last value has to be appended', () => { + const values = Array.from({ length: 300 }, (_, i) => i); + // step = ceil(300 / 60) = 5, which already lands on 60 stepped values + // (0..295) plus the appended last value (299), totaling 61 — one over + // maxTicks. Keeping the true last bucket wins over a hard cap. + expect(capTickMarks(values, 60)).toHaveLength(61); + }); +}); + test('getMinAndMaxFromBounds returns empty object when not truncating', () => { expect( getMinAndMaxFromBounds( From 58ef03dabca2c0ae4646e6a4139c8c3ebb7fef3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:12:49 -0700 Subject: [PATCH 09/12] chore(deps-dev): bump oxfmt from 0.64.0 to 0.65.0 in /superset-websocket (#43739) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-websocket/package-lock.json | 160 +++++++++++++-------------- superset-websocket/package.json | 2 +- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json index 7185250b9076..3407ab6da61a 100644 --- a/superset-websocket/package-lock.json +++ b/superset-websocket/package-lock.json @@ -29,7 +29,7 @@ "eslint": "^10.9.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.11.0", - "oxfmt": "^0.64.0", + "oxfmt": "^0.65.0", "tscw-config": "^1.1.2", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", @@ -257,9 +257,9 @@ } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", - "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz", + "integrity": "sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==", "cpu": [ "arm" ], @@ -274,9 +274,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", - "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz", + "integrity": "sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==", "cpu": [ "arm64" ], @@ -291,9 +291,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", - "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz", + "integrity": "sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==", "cpu": [ "arm64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", - "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz", + "integrity": "sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==", "cpu": [ "x64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", - "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz", + "integrity": "sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==", "cpu": [ "x64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", - "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz", + "integrity": "sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==", "cpu": [ "arm" ], @@ -359,9 +359,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", - "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz", + "integrity": "sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==", "cpu": [ "arm" ], @@ -376,9 +376,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", - "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz", + "integrity": "sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==", "cpu": [ "arm64" ], @@ -396,9 +396,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", - "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz", + "integrity": "sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==", "cpu": [ "arm64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", - "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz", + "integrity": "sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==", "cpu": [ "ppc64" ], @@ -436,9 +436,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", - "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz", + "integrity": "sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==", "cpu": [ "riscv64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", - "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz", + "integrity": "sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==", "cpu": [ "riscv64" ], @@ -476,9 +476,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", - "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz", + "integrity": "sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==", "cpu": [ "s390x" ], @@ -496,9 +496,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", - "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz", + "integrity": "sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==", "cpu": [ "x64" ], @@ -516,9 +516,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", - "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz", + "integrity": "sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==", "cpu": [ "x64" ], @@ -536,9 +536,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", - "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz", + "integrity": "sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==", "cpu": [ "arm64" ], @@ -553,9 +553,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", - "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz", + "integrity": "sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==", "cpu": [ "arm64" ], @@ -570,9 +570,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", - "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz", + "integrity": "sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==", "cpu": [ "ia32" ], @@ -587,9 +587,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", - "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz", + "integrity": "sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==", "cpu": [ "x64" ], @@ -2656,9 +2656,9 @@ } }, "node_modules/oxfmt": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", - "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", + "version": "0.65.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.65.0.tgz", + "integrity": "sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==", "dev": true, "license": "MIT", "dependencies": { @@ -2674,25 +2674,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.64.0", - "@oxfmt/binding-android-arm64": "0.64.0", - "@oxfmt/binding-darwin-arm64": "0.64.0", - "@oxfmt/binding-darwin-x64": "0.64.0", - "@oxfmt/binding-freebsd-x64": "0.64.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", - "@oxfmt/binding-linux-arm64-gnu": "0.64.0", - "@oxfmt/binding-linux-arm64-musl": "0.64.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", - "@oxfmt/binding-linux-riscv64-musl": "0.64.0", - "@oxfmt/binding-linux-s390x-gnu": "0.64.0", - "@oxfmt/binding-linux-x64-gnu": "0.64.0", - "@oxfmt/binding-linux-x64-musl": "0.64.0", - "@oxfmt/binding-openharmony-arm64": "0.64.0", - "@oxfmt/binding-win32-arm64-msvc": "0.64.0", - "@oxfmt/binding-win32-ia32-msvc": "0.64.0", - "@oxfmt/binding-win32-x64-msvc": "0.64.0" + "@oxfmt/binding-android-arm-eabi": "0.65.0", + "@oxfmt/binding-android-arm64": "0.65.0", + "@oxfmt/binding-darwin-arm64": "0.65.0", + "@oxfmt/binding-darwin-x64": "0.65.0", + "@oxfmt/binding-freebsd-x64": "0.65.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.65.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.65.0", + "@oxfmt/binding-linux-arm64-gnu": "0.65.0", + "@oxfmt/binding-linux-arm64-musl": "0.65.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.65.0", + "@oxfmt/binding-linux-riscv64-musl": "0.65.0", + "@oxfmt/binding-linux-s390x-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-gnu": "0.65.0", + "@oxfmt/binding-linux-x64-musl": "0.65.0", + "@oxfmt/binding-openharmony-arm64": "0.65.0", + "@oxfmt/binding-win32-arm64-msvc": "0.65.0", + "@oxfmt/binding-win32-ia32-msvc": "0.65.0", + "@oxfmt/binding-win32-x64-msvc": "0.65.0" }, "peerDependencies": { "svelte": "^5.0.0", diff --git a/superset-websocket/package.json b/superset-websocket/package.json index c51a31d8a264..76bf9d26890d 100644 --- a/superset-websocket/package.json +++ b/superset-websocket/package.json @@ -37,7 +37,7 @@ "eslint": "^10.9.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.11.0", - "oxfmt": "^0.64.0", + "oxfmt": "^0.65.0", "tscw-config": "^1.1.2", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", From eec1e4d288bf0d32c3d85849cd432c5bb470fb83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:13:26 -0700 Subject: [PATCH 10/12] chore(deps-dev): bump eslint from 10.9.0 to 10.9.1 in /superset-websocket (#43740) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-websocket/package-lock.json | 8 ++++---- superset-websocket/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json index 3407ab6da61a..761b9c8bd005 100644 --- a/superset-websocket/package-lock.json +++ b/superset-websocket/package-lock.json @@ -26,7 +26,7 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", - "eslint": "^10.9.0", + "eslint": "^10.9.1", "eslint-config-prettier": "^10.1.8", "globals": "^17.11.0", "oxfmt": "^0.65.0", @@ -1624,9 +1624,9 @@ } }, "node_modules/eslint": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.0.tgz", - "integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/superset-websocket/package.json b/superset-websocket/package.json index 76bf9d26890d..9b760672731a 100644 --- a/superset-websocket/package.json +++ b/superset-websocket/package.json @@ -34,7 +34,7 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", - "eslint": "^10.9.0", + "eslint": "^10.9.1", "eslint-config-prettier": "^10.1.8", "globals": "^17.11.0", "oxfmt": "^0.65.0", From ed513bbe42af8f4bb130aee0c17c2080eb0913a3 Mon Sep 17 00:00:00 2001 From: Luc Verdier Date: Tue, 1 Sep 2026 19:15:22 +0200 Subject: [PATCH 11/12] docs: add Veremes to users in the wild (#43754) Co-authored-by: Luc Verdier --- RESOURCES/INTHEWILD.yaml | 5 +++ docs/static/img/logos/veremes.svg | 51 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 docs/static/img/logos/veremes.svg diff --git a/RESOURCES/INTHEWILD.yaml b/RESOURCES/INTHEWILD.yaml index 8a17a6e7044e..33f480b7d8b8 100644 --- a/RESOURCES/INTHEWILD.yaml +++ b/RESOURCES/INTHEWILD.yaml @@ -441,6 +441,11 @@ categories: url: https://bestpair.info/ contributors: ["@stevensuting"] + - name: Veremes + url: https://www.veremes.com/ + logo: veremes.svg + contributors: ["@verdier"] + - name: Virtuoso QA url: https://www.virtuosoqa.com diff --git a/docs/static/img/logos/veremes.svg b/docs/static/img/logos/veremes.svg new file mode 100644 index 000000000000..ad75a4be868f --- /dev/null +++ b/docs/static/img/logos/veremes.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + From e683aadd6e05308e575e4189cc0963ff9b15be26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:52:17 +0700 Subject: [PATCH 12/12] chore(build): upgrade `webpack-dev-server` v7 and dependent libs (#43766) --- superset-frontend/package-lock.json | 1399 +++++++++++++-------------- superset-frontend/package.json | 6 +- 2 files changed, 662 insertions(+), 743 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 670e9571bdaa..a2e478f5e8d2 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -178,7 +178,7 @@ "@formatjs/intl-durationformat": "^0.10.18", "@istanbuljs/nyc-config-typescript": "^1.0.1", "@playwright/test": "^1.62.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.3", "@storybook/addon-docs": "10.5.10", "@storybook/addon-links": "10.5.10", "@storybook/react-webpack5": "10.5.10", @@ -277,8 +277,8 @@ "wait-on": "^9.1.0", "webpack": "^5.109.2", "webpack-bundle-analyzer": "^5.3.2", - "webpack-cli": "^7.0.3", - "webpack-dev-server": "^5.2.5", + "webpack-cli": "^7.2.3", + "webpack-dev-server": "^6.0.0", "webpack-manifest-plugin": "^6.0.1", "webpack-sources": "^3.5.1", "webpack-visualizer-plugin2": "^2.0.0" @@ -4829,6 +4829,20 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -5608,14 +5622,14 @@ } }, "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.3.tgz", - "integrity": "sha512-IvO50vkGydDZwS1e9rz/JXEtCCt9XvqxoGI6FlrVIvVm4/HpygMKW4ETtREWtMTsN5CLJ9FR6GuCduoQPZLBiw==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.2.tgz", + "integrity": "sha512-PoBeUNEbjyLKKwCap2z8LkkqdhdfttS4rTHCALVuP65BdF+sAoyBqHo1m+uGTRBiQWv3H7MGfr8f7lY4pDwanw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.3", - "@jsonjoy.com/fs-node-utils": "4.57.3", + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "@jsonjoy.com/fs-node-utils": "4.68.2", "thingies": "^2.5.0" }, "engines": { @@ -5630,15 +5644,15 @@ } }, "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.3.tgz", - "integrity": "sha512-JlIDGUWPl7Y6zl+/ISnZuh8z2aMr/xoR66D18zlaVAuL192CvlNJEzOlzp27x4P52HRtDnCSOk6f59vTsmp5vw==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.2.tgz", + "integrity": "sha512-h6eGXlLGGMyPfNliDQrbuHKTB9Z29ksCLjreAmdBqrDOBdfrTrHssi+b9WLfrF+J2KzAbIt9OMjJu6AEpTnYkg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.3", - "@jsonjoy.com/fs-node-builtins": "4.57.3", - "@jsonjoy.com/fs-node-utils": "4.57.3", + "@jsonjoy.com/fs-core": "4.68.2", + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "@jsonjoy.com/fs-node-utils": "4.68.2", "thingies": "^2.5.0" }, "engines": { @@ -5653,17 +5667,17 @@ } }, "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.3.tgz", - "integrity": "sha512-089gZoKvbeOsT2jeBaVKSz91oFXQWFG7a62sMY6gVMHnoWbyGzTb6OVUP/V7G3wLQLJ555BEsHt8SD1nj1dgaQ==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.2.tgz", + "integrity": "sha512-Kpj519Qk4OXG4+mZ8vTf9BEflliJrPueIDEVcbx8tAOufMJJ8IxR0WwdbcEvJol2KQog3PJjtALXVclorE+xbQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.3", - "@jsonjoy.com/fs-node-builtins": "4.57.3", - "@jsonjoy.com/fs-node-utils": "4.57.3", - "@jsonjoy.com/fs-print": "4.57.3", - "@jsonjoy.com/fs-snapshot": "4.57.3", + "@jsonjoy.com/fs-core": "4.68.2", + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "@jsonjoy.com/fs-node-utils": "4.68.2", + "@jsonjoy.com/fs-print": "4.68.2", + "@jsonjoy.com/fs-snapshot": "4.68.2", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -5679,9 +5693,9 @@ } }, "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.3.tgz", - "integrity": "sha512-JAI3PqNuY8BR7ovy4h0bADLrqJLIcUauONNZfyTxUnj3Wf3tpTYe39eJ6z7FzYyA+tdMt33VpiQQUikGr3QOBw==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.2.tgz", + "integrity": "sha512-V8WzQsW2YIrH3RxBGY6HZisxn+dDUltHgksVRuCdPEOXEkAfXuhL/01eHFrabNu84Dn13XuLqvcQUKOYVKAUOA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5696,15 +5710,15 @@ } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.3.tgz", - "integrity": "sha512-uZGxyC0zDmcmW5bfHd4YivAZ54BLlbF9G0K5rBaksI/tZdJSGM7/AC+1TY7yvFu0Wc6gUHR7mFwf6SbQ3J1BTQ==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.2.tgz", + "integrity": "sha512-4O1K4w5G4oJIpKpoa3WSLG83AsQYVnGv+aUSBGOvIUph9Axm6bB1mPlvldkNg2tBx/4dkiTlZnqNrK5SQ21Wtg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.3", - "@jsonjoy.com/fs-node-builtins": "4.57.3", - "@jsonjoy.com/fs-node-utils": "4.57.3" + "@jsonjoy.com/fs-fsa": "4.68.2", + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "@jsonjoy.com/fs-node-utils": "4.68.2" }, "engines": { "node": ">=10.0" @@ -5718,13 +5732,14 @@ } }, "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.3.tgz", - "integrity": "sha512-quCil8AvfcOxob4pn0drGdcQWpkPVgkt9q1+EjeyXXT40/L3l5lvYrr6hR8LmHu0eg+DNNaUwqjLT6Hr7V4sdQ==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.2.tgz", + "integrity": "sha512-CxFwyG9fJr7dAKAd1uanNRNrRMtDDqbYJA8do53+M4LY7SxcBEEmMjC1zi9MOsBlVLHTXvA7OP9+n639UqtIcw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.3" + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "glob-to-regex.js": "^1.0.1" }, "engines": { "node": ">=10.0" @@ -5738,13 +5753,13 @@ } }, "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.3.tgz", - "integrity": "sha512-ITwaLZpGIqD9jHndwMvDFZDIvbVzGRsJZDQ5HKln0vyMculu1c1nb7zbEBgY8BVSBZ9S2xO138OWIBGeRsrF3Q==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.2.tgz", + "integrity": "sha512-cBABQmZJXig6bahwNka3+yPNGUF1GeMWbR76ZM1N2ajgCd+S+twZigCoe9+0ueZmkhM9xd2a7688Gy/ch7T+2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.3", + "@jsonjoy.com/fs-node-utils": "4.68.2", "tree-dump": "^1.1.0" }, "engines": { @@ -5759,14 +5774,14 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.3.tgz", - "integrity": "sha512-wdNaG2DxCtvj9lKldAnEV3ycYPEpk+p2cP2lHD1qdxkoQGlWUtQverqvG9KZSkm6BHFha4PP6XRZbpARNfHRxA==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.2.tgz", + "integrity": "sha512-Aix7+NM38LzvewM0T3PICSgFdF5BVCtVgsjNsrlDPGc9hiMgJzReTDDl2/elgl0A9USMl3QP27x5WwxZSOtrfw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.3", + "@jsonjoy.com/fs-node-utils": "4.68.2", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -9377,9 +9392,9 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", - "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.3.tgz", + "integrity": "sha512-k1EEmFS9K7685tqJM55DCA3sYlTNZb7aFj1M3HNFwMqH+fGgMEvemIQ5kurIzKc9wYRSmfKpSZN3WUyY/GHlvg==", "dev": true, "license": "MIT", "dependencies": { @@ -9399,7 +9414,7 @@ "sockjs-client": "^1.4.0", "type-fest": ">=0.17.0 <6.0.0", "webpack": "^5.0.0", - "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-dev-server": "^4.8.0 || 5.x || 6.x", "webpack-hot-middleware": "2.x", "webpack-plugin-serve": "1.x" }, @@ -10619,16 +10634,6 @@ "node": ">=8" } }, - "node_modules/@sigstore/sign/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@sigstore/sign/node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -13082,16 +13087,6 @@ "@types/send": "*" } }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/source-list-map": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", @@ -14710,19 +14705,46 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ace-builds": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.44.0.tgz", @@ -15172,13 +15194,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -15870,6 +15885,7 @@ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8" }, @@ -15912,55 +15928,38 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">=18" }, "funding": { "type": "opencollective", @@ -15968,33 +15967,20 @@ } }, "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bonjour-service": { @@ -16811,6 +16797,7 @@ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -17647,9 +17634,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -17657,11 +17644,14 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/copy-to-clipboard": { "version": "3.3.3", @@ -18854,17 +18844,6 @@ "node": ">=6" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -18885,13 +18864,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -20643,46 +20615,43 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -20690,35 +20659,46 @@ } }, "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -20916,19 +20896,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -21103,41 +21070,27 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -21604,13 +21557,13 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fromentries": { @@ -22912,13 +22865,6 @@ "yarn": ">=1.3.0" } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -23383,59 +23329,6 @@ "node": "20 || >=22" } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -23668,37 +23561,27 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" - }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/http-parser-js": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", - "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", - "dev": true, - "license": "MIT" - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -24258,6 +24141,7 @@ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -24549,9 +24433,9 @@ } }, "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", "dev": true, "license": "MIT", "engines": { @@ -24618,6 +24502,13 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -26736,20 +26627,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", - "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/js-yaml-loader": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/js-yaml-loader/-/js-yaml-loader-1.2.2.tgz", @@ -26762,6 +26639,20 @@ "un-eval": "^1.2.0" } }, + "node_modules/js-yaml-loader/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/js-yaml-loader/node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -28466,16 +28357,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -28975,13 +28856,17 @@ "license": "CC0-1.0" }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mem-fs": { @@ -29105,11 +28990,14 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -29148,16 +29036,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mgrs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", @@ -29740,19 +29618,6 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -29816,13 +29681,6 @@ "webpack": "^5.0.0" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, "node_modules/minimatch": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", @@ -30276,13 +30134,34 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "dev": true, "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/neo-async": { @@ -31543,13 +31422,6 @@ "dev": true, "license": "ISC" }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/ol": { "version": "10.10.0", "resolved": "https://registry.npmjs.org/ol/-/ol-10.10.0.tgz", @@ -32047,6 +31919,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", + "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-network-error": "^1.3.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -32558,11 +32446,15 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -33490,65 +33382,38 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/rbush": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", @@ -35473,6 +35338,7 @@ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -36179,16 +36045,6 @@ "node": ">=8" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -36238,6 +36094,23 @@ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -36450,13 +36323,6 @@ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", "license": "MIT" }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, "node_modules/selfsigned": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", @@ -36484,55 +36350,57 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serialize-javascript": { @@ -36552,22 +36420,40 @@ "license": "ISC" }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/debug": { @@ -36591,28 +36477,22 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -36620,12 +36500,15 @@ "dev": true, "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "node_modules/serve-index/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", @@ -36638,19 +36521,23 @@ } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-blocking": { @@ -37059,18 +36946,6 @@ "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==", "license": "MIT" }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -37317,38 +37192,6 @@ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, "node_modules/specificity": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", @@ -37713,9 +37556,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -39085,9 +38928,9 @@ } }, "node_modules/thingies": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", - "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", "dev": true, "license": "MIT", "engines": { @@ -39801,19 +39644,65 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -40632,16 +40521,6 @@ "dev": true, "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -41016,16 +40895,6 @@ "node": ">=10.13.0" } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -41184,17 +41053,17 @@ "license": "MIT" }, "node_modules/webpack-cli": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.0.3.tgz", - "integrity": "sha512-2E2C6A1e2El7791zQgTH7LPIuwLjRliow9OHS/qlJc9pwhZlCoL/uiwqd/1WSlXT83wJfmfDbkcqHXuXoPJZ3g==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-7.2.3.tgz", + "integrity": "sha512-vDFU7jrfCctnN7jJQWPl+V26B51GLp11prVZXg50oeonsgeBzSTJEWmXkjHsOjTgMjlPOQM8GWLh33X9RN/0ow==", "dev": true, "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^1.1.0", "commander": "^14.0.3", "cross-spawn": "^7.0.6", - "envinfo": "^7.14.0", - "import-local": "^3.0.2", + "envinfo": "^7.21.0", + "import-local": "^3.2.0", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^6.0.1" @@ -41210,11 +41079,23 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "js-yaml": "^4.0.0 || ^5.0.0", + "json5": "^2.2.3", + "toml": "^3.0.0 || ^4.0.0 || ^5.0.0", "webpack": "^5.101.0", "webpack-bundle-analyzer": "^4.0.0 || ^5.0.0", - "webpack-dev-server": "^5.0.0" + "webpack-dev-server": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { + "js-yaml": { + "optional": true + }, + "json5": { + "optional": true + }, + "toml": { + "optional": true + }, "webpack-bundle-analyzer": { "optional": true }, @@ -41286,53 +41167,50 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz", - "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-6.0.0.tgz", + "integrity": "sha512-q9SD4ItOGhZLeU6EGT10caDZdHjF50Pz1DtkRZZOPsfluMXOkacWKKOtSBSLVkPqKiF67eFUC0rI88U/tSFPEw==", "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.25", - "@types/express-serve-static-core": "^4.17.21", + "@types/express": "^5.0.6", + "@types/express-serve-static-core": "^5.1.1", "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", + "@types/serve-static": "^2.2.0", + "@types/ws": "^8.18.1", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", + "bonjour-service": "^1.3.0", + "chokidar": "^5.0.0", "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.22.1", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", + "express": "^5.2.1", + "graceful-fs": "^4.2.11", + "http-proxy-middleware": "^4.1.1", + "ipaddr.js": "^2.3.0", "launch-editor": "^2.14.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", + "open": "^11.0.0", + "p-retry": "^8.0.0", + "schema-utils": "^4.3.3", "selfsigned": "^5.5.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" + "serve-index": "^1.9.2", + "tinyglobby": "^0.2.15", + "webpack-dev-middleware": "^8.0.3", + "ws": "^8.20.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 22.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { @@ -41343,12 +41221,41 @@ } } }, - "node_modules/webpack-dev-server/node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "node_modules/webpack-dev-server/node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/webpack-dev-server/node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } }, "node_modules/webpack-dev-server/node_modules/@types/ws": { "version": "8.18.1", @@ -41360,47 +41267,64 @@ "@types/node": "*" } }, - "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, "engines": { - "node": ">= 10" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/webpack-dev-server/node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "node_modules/webpack-dev-server/node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", "dependencies": { - "is-inside-container": "^1.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "4.57.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.3.tgz", - "integrity": "sha512-dlvqataP1zUOlfj6pv9wgCSC5pRIooNntXgdLfR7FWlcKi1p8fMfJADtHp/+8Dhu5JFvMHNh7L0QVcuaaBKqqA==", + "version": "4.68.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.2.tgz", + "integrity": "sha512-Un1ElEBoIdPI9kg0sm3LebVEuEViodfIvlaG8Z1MFXa7ZnR9vWuPKUo3dt/vuWY2ivc05l76B5QOjdgCzAzmGw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.3", - "@jsonjoy.com/fs-fsa": "4.57.3", - "@jsonjoy.com/fs-node": "4.57.3", - "@jsonjoy.com/fs-node-builtins": "4.57.3", - "@jsonjoy.com/fs-node-to-fsa": "4.57.3", - "@jsonjoy.com/fs-node-utils": "4.57.3", - "@jsonjoy.com/fs-print": "4.57.3", - "@jsonjoy.com/fs-snapshot": "4.57.3", + "@jsonjoy.com/fs-core": "4.68.2", + "@jsonjoy.com/fs-fsa": "4.68.2", + "@jsonjoy.com/fs-node": "4.68.2", + "@jsonjoy.com/fs-node-builtins": "4.68.2", + "@jsonjoy.com/fs-node-to-fsa": "4.68.2", + "@jsonjoy.com/fs-node-utils": "4.68.2", + "@jsonjoy.com/fs-print": "4.68.2", + "@jsonjoy.com/fs-snapshot": "4.68.2", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -41411,9 +41335,6 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" } }, "node_modules/webpack-dev-server/node_modules/mime-db": { @@ -41444,65 +41365,74 @@ } }, "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.2.tgz", + "integrity": "sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "node_modules/webpack-dev-server/node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", "dev": true, "license": "MIT", - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, "engines": { - "node": ">=16.17" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", - "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.1.1.tgz", + "integrity": "sha512-JrxAE/HwNmEnTkzkUGHFItHpGalzuEIUDifXhjAkIEHZzHK/ZJFVoTQF5ubZQlgeRireqLdr2lZttrrmOH61IA==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.43.1", - "mime-types": "^3.0.1", - "on-finished": "^2.4.1", + "memfs": "^4.56.10", + "mime-types": "^3.0.2", "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "schema-utils": "^4.3.3" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.0.0" + "webpack": "^5.101.0" }, "peerDependenciesMeta": { "webpack": { @@ -41511,16 +41441,30 @@ } }, "node_modules/webpack-dev-server/node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", + "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", "dev": true, "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0" + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -41647,31 +41591,6 @@ "node": ">= 0.6" } }, - "node_modules/websocket-driver": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", - "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/wgsl_reflect": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 7ec39fe22127..986502daff7c 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -255,7 +255,7 @@ "@formatjs/intl-durationformat": "^0.10.18", "@istanbuljs/nyc-config-typescript": "^1.0.1", "@playwright/test": "^1.62.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.3", "@storybook/addon-docs": "10.5.10", "@storybook/addon-links": "10.5.10", "@storybook/react-webpack5": "10.5.10", @@ -354,8 +354,8 @@ "wait-on": "^9.1.0", "webpack": "^5.109.2", "webpack-bundle-analyzer": "^5.3.2", - "webpack-cli": "^7.0.3", - "webpack-dev-server": "^5.2.5", + "webpack-cli": "^7.2.3", + "webpack-dev-server": "^6.0.0", "webpack-manifest-plugin": "^6.0.1", "webpack-sources": "^3.5.1", "webpack-visualizer-plugin2": "^2.0.0"