From 555959d3f8c3a50f79f93e4766775ddc32544ee7 Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Wed, 6 May 2026 10:41:36 +0200 Subject: [PATCH 1/8] fix: use real IP in OTEL traces (#5278) * fix: use real IP in OTEL traces * address feedback --- backend/src/baserow/api/sessions.py | 15 +----- backend/src/baserow/api/user/views.py | 2 +- .../src/baserow/core/telemetry/telemetry.py | 16 ++++++- backend/src/baserow/core/utils.py | 41 +++++++++++++++- backend/src/baserow/throttling/handler.py | 2 +- backend/src/baserow/throttling/middleware.py | 2 +- .../baserow/core/telemetry/test_telemetry.py | 47 +++++++++++++++++++ ...nt_ip_in_opentelemetry_request_traces.json | 9 ++++ 8 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 backend/tests/baserow/core/telemetry/test_telemetry.py create mode 100644 changelog/entries/unreleased/bug/5277_use_real_client_ip_in_opentelemetry_request_traces.json diff --git a/backend/src/baserow/api/sessions.py b/backend/src/baserow/api/sessions.py index a92d64ea9b..19d9f15529 100644 --- a/backend/src/baserow/api/sessions.py +++ b/backend/src/baserow/api/sessions.py @@ -9,6 +9,7 @@ InvalidClientSessionIdAPIException, InvalidUndoRedoActionGroupIdAPIException, ) +from baserow.core.utils import get_user_remote_ip_address_from_request UNTRUSTED_CLIENT_SESSION_ID_USER_ATTR = "untrusted_client_session_id" UNDO_REDO_ACTION_GROUP_ID = "untrusted_client_action_group" @@ -95,20 +96,6 @@ def _set_user_websocket_id(user, websocket_id): user.web_socket_id = websocket_id -def get_user_remote_ip_address_from_request(request): - x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") - if x_forwarded_for: - # X-Forwarded-For can contain multiple IPs: client, proxy1, proxy2. - # The first one is the original client IP. - return x_forwarded_for.split(",")[0].strip() - - x_real_ip = request.META.get("HTTP_X_REAL_IP") - if x_real_ip: - return x_real_ip.strip() - - return request.META.get("REMOTE_ADDR") - - def set_user_remote_addr_ip_from_request(user, request): ip_address = get_user_remote_ip_address_from_request(request) set_user_remote_addr_ip(user, ip_address) diff --git a/backend/src/baserow/api/user/views.py b/backend/src/baserow/api/user/views.py index cef3a924b0..2dd3bad5ad 100755 --- a/backend/src/baserow/api/user/views.py +++ b/backend/src/baserow/api/user/views.py @@ -36,7 +36,6 @@ from baserow.api.schemas import get_error_schema from baserow.api.sessions import ( get_untrusted_client_session_id, - get_user_remote_ip_address_from_request, set_user_session_data_from_request, ) from baserow.api.user.registries import user_data_registry @@ -90,6 +89,7 @@ ) from baserow.core.user.handler import UserHandler from baserow.core.user.utils import generate_session_tokens_for_user +from baserow.core.utils import get_user_remote_ip_address_from_request from .errors import ( ERROR_ALREADY_EXISTS, diff --git a/backend/src/baserow/core/telemetry/telemetry.py b/backend/src/baserow/core/telemetry/telemetry.py index 04e7c43c5a..dbc8ffbaca 100644 --- a/backend/src/baserow/core/telemetry/telemetry.py +++ b/backend/src/baserow/core/telemetry/telemetry.py @@ -1,14 +1,20 @@ import logging import sys +from django.http import HttpRequest + from celery import signals from opentelemetry import metrics, trace from opentelemetry._logs import set_logger_provider from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.trace import Span from baserow.core.psycopg import is_psycopg3 from baserow.core.telemetry.provider import DifferentSamplerPerLibraryTracerProvider from baserow.core.telemetry.utils import BatchBaggageSpanProcessor, otel_is_enabled +from baserow.core.utils import get_user_remote_ip_address_from_request + +OTEL_CLIENT_IP_ATTRIBUTE_NAMES = ("client.address", "net.peer.ip") class LogGuruCompatibleLoggerHandler(LoggingHandler): @@ -167,4 +173,12 @@ def _setup_standard_backend_instrumentation(): def _setup_django_process_instrumentation(): from opentelemetry.instrumentation.django import DjangoInstrumentor - DjangoInstrumentor().instrument() + DjangoInstrumentor().instrument(request_hook=_set_real_client_ip_on_request_span) + + +def _set_real_client_ip_on_request_span(span: Span, request: HttpRequest): + if span and span.is_recording(): + ip_address = get_user_remote_ip_address_from_request(request) + if ip_address: + for attribute_name in OTEL_CLIENT_IP_ATTRIBUTE_NAMES: + span.set_attribute(attribute_name, ip_address) diff --git a/backend/src/baserow/core/utils.py b/backend/src/baserow/core/utils.py index ea333ad7d3..afc73fd42a 100644 --- a/backend/src/baserow/core/utils.py +++ b/backend/src/baserow/core/utils.py @@ -16,7 +16,18 @@ from fractions import Fraction from itertools import chain, islice from numbers import Number -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + Union, +) from django.conf import settings from django.db import transaction @@ -30,6 +41,9 @@ from .exceptions import CannotCalculateIntermediateOrder +if TYPE_CHECKING: + from django.http import HttpRequest + RE_ESCAPE_CHAR = re.compile(r"\\(\\)?") RE_PROP_NAME = re.compile( # Match anything that isn't a dot or bracket. @@ -61,6 +75,31 @@ def flatten(nested_list: List[Any]): ] +def get_user_remote_ip_address_from_request( + request: HttpRequest, +) -> Optional[str]: + """ + Extracts the remote IP address of the user from the request. It checks for + X-Forwarded-For and X-Real-IP headers first (commonly set by proxies), and falls + back to the REMOTE_ADDR if neither is available. + + :param request: The HTTP request object. + :return: The remote IP address as a string, or None if it is unavailable. + """ + + x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") + if x_forwarded_for: + # X-Forwarded-For can contain multiple IPs: client, proxy1, proxy2. + # The first one is the original client IP. + return x_forwarded_for.split(",")[0].strip() + + x_real_ip = request.META.get("HTTP_X_REAL_IP") + if x_real_ip: + return x_real_ip.strip() + + return request.META.get("REMOTE_ADDR") + + def split_attrs_and_m2m_fields( field_names: List[str], instance: Type[Model] ) -> Tuple[List[str], List[str]]: diff --git a/backend/src/baserow/throttling/handler.py b/backend/src/baserow/throttling/handler.py index 8b99d84baf..1593eb7939 100644 --- a/backend/src/baserow/throttling/handler.py +++ b/backend/src/baserow/throttling/handler.py @@ -11,7 +11,7 @@ from rest_framework.throttling import SimpleRateThrottle from baserow.api.exceptions import ThrottledAPIException -from baserow.api.sessions import get_user_remote_ip_address_from_request +from baserow.core.utils import get_user_remote_ip_address_from_request from .blacklist import blacklist_ip, blacklist_token from .exceptions import RateLimitExceededException diff --git a/backend/src/baserow/throttling/middleware.py b/backend/src/baserow/throttling/middleware.py index 36b783c64c..65c2211940 100644 --- a/backend/src/baserow/throttling/middleware.py +++ b/backend/src/baserow/throttling/middleware.py @@ -7,7 +7,7 @@ ThrottledAPIException, api_exception_to_json_response, ) -from baserow.api.sessions import get_user_remote_ip_address_from_request +from baserow.core.utils import get_user_remote_ip_address_from_request from baserow.throttling.handler import ConcurrentUserRequestsThrottle from .blacklist import get_token_cooldown_time, is_ip_blacklisted diff --git a/backend/tests/baserow/core/telemetry/test_telemetry.py b/backend/tests/baserow/core/telemetry/test_telemetry.py new file mode 100644 index 0000000000..e02ecdc984 --- /dev/null +++ b/backend/tests/baserow/core/telemetry/test_telemetry.py @@ -0,0 +1,47 @@ +from unittest.mock import MagicMock, patch + +from baserow.core.telemetry.telemetry import ( + _set_real_client_ip_on_request_span, + _setup_django_process_instrumentation, +) + + +def test_set_real_client_ip_on_request_span_uses_forwarded_for(api_request_factory): + request = api_request_factory.get( + "/api/workspaces/", + REMOTE_ADDR="10.0.0.1", + HTTP_X_FORWARDED_FOR="203.0.113.50, 70.41.3.18", + ) + span = MagicMock() + span.is_recording.return_value = True + + _set_real_client_ip_on_request_span(span, request) + + span.set_attribute.assert_any_call("client.address", "203.0.113.50") + span.set_attribute.assert_any_call("net.peer.ip", "203.0.113.50") + assert span.set_attribute.call_count == 2 + + +def test_set_real_client_ip_on_request_span_falls_back_to_real_ip(api_request_factory): + request = api_request_factory.get( + "/api/workspaces/", + REMOTE_ADDR="10.0.0.1", + HTTP_X_REAL_IP="203.0.113.51", + ) + span = MagicMock() + span.is_recording.return_value = True + + _set_real_client_ip_on_request_span(span, request) + + span.set_attribute.assert_any_call("client.address", "203.0.113.51") + span.set_attribute.assert_any_call("net.peer.ip", "203.0.113.51") + assert span.set_attribute.call_count == 2 + + +def test_setup_django_process_instrumentation_registers_real_ip_request_hook(): + with patch( + "opentelemetry.instrumentation.django.DjangoInstrumentor.instrument" + ) as instrument: + _setup_django_process_instrumentation() + + instrument.assert_called_once_with(request_hook=_set_real_client_ip_on_request_span) diff --git a/changelog/entries/unreleased/bug/5277_use_real_client_ip_in_opentelemetry_request_traces.json b/changelog/entries/unreleased/bug/5277_use_real_client_ip_in_opentelemetry_request_traces.json new file mode 100644 index 0000000000..b3ed3840e6 --- /dev/null +++ b/changelog/entries/unreleased/bug/5277_use_real_client_ip_in_opentelemetry_request_traces.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Use the real client IP address in OpenTelemetry request traces when proxy headers are present.", + "issue_origin": "github", + "issue_number": 5277, + "domain": "core", + "bullet_points": [], + "created_at": "2026-04-29" +} From d96a416f7158bda60303e18098c5dbd24610ed4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 13:00:46 +0200 Subject: [PATCH 2/8] chore(deps): bump python-dotenv from 1.2.1 to 1.2.2 in /backend (#5247) Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: python-dotenv dependency-version: 1.2.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- backend/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/uv.lock b/backend/uv.lock index 90188c26bf..e6fd5349ce 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -3190,11 +3190,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] From 08abafb8182cefdcd72fdbf1be4f579cef91c9e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 13:01:37 +0200 Subject: [PATCH 3/8] chore(deps): bump postcss from 8.5.6 to 8.5.12 in /web-frontend (#5273) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.6 to 8.5.12. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.6...8.5.12) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web-frontend/yarn.lock | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock index e5e2aa0a53..3f1f178af7 100644 --- a/web-frontend/yarn.lock +++ b/web-frontend/yarn.lock @@ -10643,19 +10643,10 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.24: - version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" - integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== - dependencies: - nanoid "^3.3.11" - picocolors "^1.1.1" - source-map-js "^1.2.1" - -postcss@^8.5.6, postcss@^8.5.8: - version "8.5.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" - integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== +postcss@^8.4.24, postcss@^8.5.6, postcss@^8.5.8: + version "8.5.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" + integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" From 14dd0893e07a3efaaa4c0a471d4a952e28b2af0c Mon Sep 17 00:00:00 2001 From: Davide Silvestri <75379892+silvestrid@users.noreply.github.com> Date: Wed, 6 May 2026 13:46:33 +0200 Subject: [PATCH 4/8] fix: count linked array primary fields (#5279) * fix: count linked array primary fields * address copilot feedback --- .../database/formula/ast/function_defs.py | 4 +- .../database/field/test_formula_field_type.py | 113 ++++++++++++++++++ ...array_for_linked_array_primary_fields.json | 9 ++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 changelog/entries/unreleased/bug/5276_fix_count_formula_returning_array_for_linked_array_primary_fields.json diff --git a/backend/src/baserow/contrib/database/formula/ast/function_defs.py b/backend/src/baserow/contrib/database/formula/ast/function_defs.py index 4023f6d5c2..7ce4dcbb41 100644 --- a/backend/src/baserow/contrib/database/formula/ast/function_defs.py +++ b/backend/src/baserow/contrib/database/formula/ast/function_defs.py @@ -2423,10 +2423,10 @@ def type_function( func_call: BaserowFunctionCall[UnTyped], arg: BaserowExpression[BaserowFormulaValidType], ) -> BaserowExpression[BaserowFormulaType]: - if BaserowGetFileCount().can_accept_arg(arg): + if BaserowGetFileCount().can_accept_arg(arg) and not arg.many: return BaserowGetFileCount()(arg) - if isinstance(arg.expression_type, BaserowFormulaArrayType): + if isinstance(arg.expression_type, BaserowFormulaArrayType) and not arg.many: return BaserowArrayLength()(arg) return arg.expression_type.count(func_call, arg).with_valid_type( diff --git a/backend/tests/baserow/contrib/database/field/test_formula_field_type.py b/backend/tests/baserow/contrib/database/field/test_formula_field_type.py index adbd02218c..2d09cb6b4d 100644 --- a/backend/tests/baserow/contrib/database/field/test_formula_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_formula_field_type.py @@ -2340,3 +2340,116 @@ def test_count_formula_for_link_row_field_with_null_values(data_fixture): ) assert getattr(row_a, formula_field.db_column) == 3 + + +@pytest.mark.django_db +def test_count_formula_for_link_row_field_with_array_primary_field(data_fixture): + """ + A links to B, and B's primary field is a lookup array through a B to C link. + count(field('')) must count linked B rows, not each B row's inner + primary lookup array length (github issue #5276) + """ + + user = data_fixture.create_user() + + table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user) + table_b_primary = table_b.field_set.get(primary=True) + table_b_primary.primary = False + table_b_primary.save() + + table_c = data_fixture.create_database_table(user=user) + primary_c = data_fixture.create_text_field( + table=table_c, name="Field C", primary=True + ) + link_b_to_c = data_fixture.create_link_row_field( + table=table_b, + link_row_table=table_c, + name="Link C", + ) + data_fixture.create_formula_field( + table=table_b, + name="Primary lookup", + primary=True, + formula=f"lookup('{link_b_to_c.name}', '{primary_c.name}')", + ) + count_formula = data_fixture.create_formula_field( + table=table_a, + name="Count", + formula=f"count(field('{link_a_to_b.name}'))", + ) + + row_handler = RowHandler() + rows_c = row_handler.force_create_rows( + user=user, + table=table_c, + rows_values=[{primary_c.db_column: "C1"}, {primary_c.db_column: "C2"}], + model=table_c.get_model(), + ).created_rows + rows_b = row_handler.force_create_rows( + user=user, + table=table_b, + rows_values=[ + {link_b_to_c.db_column: [rows_c[0].id]}, + {link_b_to_c.db_column: [rows_c[1].id]}, + ], + model=table_b.get_model(), + ).created_rows + row_a = row_handler.force_create_rows( + user=user, + table=table_a, + rows_values=[{link_a_to_b.db_column: [row.id for row in rows_b]}], + model=table_a.get_model(), + ).created_rows[0] + + assert getattr(row_a, count_formula.db_column) == 2 + + +@pytest.mark.django_db +def test_count_formula_for_link_row_field_with_file_primary_field(data_fixture): + """ + A links to B, and B's primary field is a file field. count(field('')) + must count linked B rows, not each B row's primary file count. + """ + + user = data_fixture.create_user() + + table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user) + table_b_primary = table_b.field_set.get(primary=True) + table_b_primary.primary = False + table_b_primary.save() + + primary_b = data_fixture.create_file_field( + table=table_b, name="Files", primary=True + ) + count_formula = data_fixture.create_formula_field( + table=table_a, + name="Count", + formula=f"count(field('{link_a_to_b.name}'))", + ) + + user_file_1 = data_fixture.create_user_file() + user_file_2 = data_fixture.create_user_file() + user_file_3 = data_fixture.create_user_file() + row_handler = RowHandler() + rows_b = row_handler.force_create_rows( + user=user, + table=table_b, + rows_values=[ + { + primary_b.db_column: [ + {"name": user_file_1.name}, + {"name": user_file_2.name}, + ] + }, + {primary_b.db_column: [{"name": user_file_3.name}]}, + ], + model=table_b.get_model(), + ).created_rows + row_a = row_handler.force_create_rows( + user=user, + table=table_a, + rows_values=[{link_a_to_b.db_column: [row.id for row in rows_b]}], + model=table_a.get_model(), + ).created_rows[0] + + assert getattr(row_a, count_formula.db_column) == 2 diff --git a/changelog/entries/unreleased/bug/5276_fix_count_formula_returning_array_for_linked_array_primary_fields.json b/changelog/entries/unreleased/bug/5276_fix_count_formula_returning_array_for_linked_array_primary_fields.json new file mode 100644 index 0000000000..bee3425b39 --- /dev/null +++ b/changelog/entries/unreleased/bug/5276_fix_count_formula_returning_array_for_linked_array_primary_fields.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed count() returning an array instead of a number for link row fields whose linked primary field is an array formula or lookup.", + "issue_origin": "github", + "issue_number": 5276, + "domain": "database", + "bullet_points": [], + "created_at": "2026-04-29" +} From ae636c2fa0eaa83a59baeaa6037bf7bab4ec19db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 13:47:05 +0200 Subject: [PATCH 5/8] chore(deps): bump axios from 1.15.0 to 1.15.2 in /web-frontend (#5317) Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web-frontend/package.json | 2 +- web-frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web-frontend/package.json b/web-frontend/package.json index 506365babe..8572e1cadf 100644 --- a/web-frontend/package.json +++ b/web-frontend/package.json @@ -80,7 +80,7 @@ "@zip.js/zip.js": "^2.8.14", "antlr4": "4.9.3", "async-mutex": "0.4.0", - "axios": "1.15.0", + "axios": "1.15.2", "bignumber.js": "9.1.1", "chart.js": "3.9.1", "chartjs-adapter-moment": "1.0.1", diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock index 3f1f178af7..80637b1171 100644 --- a/web-frontend/yarn.lock +++ b/web-frontend/yarn.lock @@ -5742,10 +5742,10 @@ axios-mock-adapter@^2.1.0: fast-deep-equal "^3.1.3" is-buffer "^2.0.5" -axios@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f" - integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q== +axios@1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b" + integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A== dependencies: follow-redirects "^1.15.11" form-data "^4.0.5" From 3b9730de5da43623cc7c6cd98308530ab9edccb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 13:48:28 +0200 Subject: [PATCH 6/8] chore(deps): bump simple-git from 3.33.0 to 3.36.0 in /web-frontend (#5318) Bumps [simple-git](https://github.com/steveukx/git-js/tree/HEAD/simple-git) from 3.33.0 to 3.36.0. - [Release notes](https://github.com/steveukx/git-js/releases) - [Changelog](https://github.com/steveukx/git-js/blob/main/simple-git/CHANGELOG.md) - [Commits](https://github.com/steveukx/git-js/commits/simple-git@3.36.0/simple-git) --- updated-dependencies: - dependency-name: simple-git dependency-version: 3.36.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web-frontend/yarn.lock | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web-frontend/yarn.lock b/web-frontend/yarn.lock index 80637b1171..7191fae656 100644 --- a/web-frontend/yarn.lock +++ b/web-frontend/yarn.lock @@ -3961,6 +3961,18 @@ "@sentry/browser" "10.46.0" "@sentry/core" "10.46.0" +"@simple-git/args-pathspec@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz#9ef4a2ad5f49ab4056362d03f93f775b93118ca5" + integrity sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA== + +"@simple-git/argv-parser@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz#275b839c6eeb5030872c73b1ea839a416885da9d" + integrity sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw== + dependencies: + "@simple-git/args-pathspec" "^1.0.3" + "@sindresorhus/base62@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@sindresorhus/base62/-/base62-1.0.0.tgz#c47c42410e5212e4fa4657670e118ddfba39acd6" @@ -11626,12 +11638,14 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-git@^3.33.0: - version "3.33.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.33.0.tgz#b903dc70f5b93535a4f64ff39172da43058cfb88" - integrity sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng== + version "3.36.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.36.0.tgz#019b28c0a35847ee34299c6fb63770ab1b2dffb7" + integrity sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q== dependencies: "@kwsites/file-exists" "^1.1.1" "@kwsites/promise-deferred" "^1.1.1" + "@simple-git/args-pathspec" "^1.0.3" + "@simple-git/argv-parser" "^1.1.0" debug "^4.4.0" sirv@^3.0.1, sirv@^3.0.2: From 3b56679cb8497add3f29426d3bae1ce1152edeec Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 6 May 2026 14:34:44 +0200 Subject: [PATCH 7/8] feat: Add Excel import (#5265) --- .../unreleased/feature/423_excel_import.json | 7 + web-frontend/locales/en.json | 3 +- .../onboarding/DatabaseImportStep.vue | 6 + .../database/components/table/CreateTable.vue | 6 + .../components/table/CreateTableModal.vue | 1 + .../components/table/TableExcelImporter.vue | 328 ++++++++++++++++++ .../modules/database/importerTypes.js | 20 ++ web-frontend/modules/database/locales/en.json | 12 + web-frontend/modules/database/plugin.js | 2 + web-frontend/modules/database/utils/excel.js | 111 ++++++ web-frontend/package.json | 3 +- .../test/unit/database/utils/excel.spec.js | 207 +++++++++++ web-frontend/yarn.lock | 4 + 13 files changed, 708 insertions(+), 2 deletions(-) create mode 100644 changelog/entries/unreleased/feature/423_excel_import.json create mode 100644 web-frontend/modules/database/components/table/TableExcelImporter.vue create mode 100644 web-frontend/modules/database/utils/excel.js create mode 100644 web-frontend/test/unit/database/utils/excel.spec.js diff --git a/changelog/entries/unreleased/feature/423_excel_import.json b/changelog/entries/unreleased/feature/423_excel_import.json new file mode 100644 index 0000000000..718ee22c83 --- /dev/null +++ b/changelog/entries/unreleased/feature/423_excel_import.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "message": "Add support for importing Excel files into database tables", + "domain": "database", + "bullet_points": [], + "created_at": "2026-04-26" +} diff --git a/web-frontend/locales/en.json b/web-frontend/locales/en.json index 33c1eef2a6..90f1bcd401 100644 --- a/web-frontend/locales/en.json +++ b/web-frontend/locales/en.json @@ -476,7 +476,8 @@ "csv": "Import a CSV file", "paste": "Paste table data", "xml": "Import an XML file", - "json": "Import a JSON file" + "json": "Import a JSON file", + "excel": "Import an Excel file" }, "apiDocs": { "intro": "Introduction", diff --git a/web-frontend/modules/database/components/onboarding/DatabaseImportStep.vue b/web-frontend/modules/database/components/onboarding/DatabaseImportStep.vue index 098b03d598..807650f3e7 100644 --- a/web-frontend/modules/database/components/onboarding/DatabaseImportStep.vue +++ b/web-frontend/modules/database/components/onboarding/DatabaseImportStep.vue @@ -55,6 +55,7 @@ v-if="dataLoaded" :rows="previewFileData" :fields="fileFields" + :field-options="fileFieldOptions" :border="true" /> @@ -103,6 +104,11 @@ export default { order: index, })) }, + fileFieldOptions() { + return Object.fromEntries( + this.fileFields.map((field) => [field.id, { hidden: false }]) + ) + }, previewFileData() { return this.previewData.map((row) => { const newRow = Object.fromEntries( diff --git a/web-frontend/modules/database/components/table/CreateTable.vue b/web-frontend/modules/database/components/table/CreateTable.vue index dd0d5d55c6..05bc82176c 100644 --- a/web-frontend/modules/database/components/table/CreateTable.vue +++ b/web-frontend/modules/database/components/table/CreateTable.vue @@ -23,6 +23,7 @@ class="import-modal__preview margin-bottom-2" :rows="previewFileData" :fields="fileFields" + :field-options="fileFieldOptions" />