diff --git a/changelog/entries/unreleased/bug/4888_migrate_jira_sync_table_endpoint.json b/changelog/entries/unreleased/bug/4888_migrate_jira_sync_table_endpoint.json new file mode 100644 index 0000000000..93c9321846 --- /dev/null +++ b/changelog/entries/unreleased/bug/4888_migrate_jira_sync_table_endpoint.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Migrate jira sync table endpoint", + "issue_origin": "github", + "issue_number": 4888, + "domain": "database", + "bullet_points": [], + "created_at": "2026-03-02" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/4898_fix_number_formatting_for_row_history_entries.json b/changelog/entries/unreleased/bug/4898_fix_number_formatting_for_row_history_entries.json new file mode 100644 index 0000000000..79d9b64812 --- /dev/null +++ b/changelog/entries/unreleased/bug/4898_fix_number_formatting_for_row_history_entries.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix number formatting for row history entries", + "issue_origin": "github", + "issue_number": 4898, + "domain": "database", + "bullet_points": [], + "created_at": "2026-03-03" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/re_add_docker_entrypoint_in_web_frontend_image_for_backward_c.json b/changelog/entries/unreleased/bug/re_add_docker_entrypoint_in_web_frontend_image_for_backward_c.json new file mode 100644 index 0000000000..f7001a5555 --- /dev/null +++ b/changelog/entries/unreleased/bug/re_add_docker_entrypoint_in_web_frontend_image_for_backward_c.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Re-add docker entrypoint in the web-frontend image for backward compatibility with custom docker-compose or Helm charts that override the container command.", + "issue_origin": "github", + "issue_number": null, + "domain": "core", + "bullet_points": [], + "created_at": "2026-03-04" +} diff --git a/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py b/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py index 2d717e7c43..4ceb445839 100644 --- a/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py +++ b/enterprise/backend/src/baserow_enterprise/data_sync/jira_issues_data_sync.py @@ -221,8 +221,26 @@ def _parse_datetime(self, value): except ValueError: raise SyncError(f"The date {value} could not be parsed.") + def _get_issue_count(self, instance, jql, headers, kwargs): + """Get approximate issue count for progress tracking.""" + + url = f"{instance.jira_url}/rest/api/2/search/approximate-count" + try: + response = advocate.post( + url, + headers={**headers, "Content-Type": "application/json"}, + json={"jql": jql}, + timeout=10, + **kwargs, + ) + if response.ok: + return response.json().get("count", 0) + except (RequestException, UnacceptableAddressException, ConnectionError): + pass + return 0 + def _fetch_issues(self, instance, progress_builder: ChildProgressBuilder): - headers = {"Content-Type": "application/json"} + headers = {"Accept": "application/json"} kwargs = {} if instance.jira_authentication == JIRA_ISSUES_DATA_SYNC_PERSONAL_ACCESS_TOKEN: @@ -234,21 +252,35 @@ def _fetch_issues(self, instance, progress_builder: ChildProgressBuilder): ) issues = [] - start_at = 0 max_results = 50 - progress = None + next_page_token = None + + if instance.jira_project_key: + jql = f"project={instance.jira_project_key} ORDER BY created DESC" + else: + jql = "created IS NOT EMPTY ORDER BY created DESC" + + issue_count = self._get_issue_count(instance, jql, headers, kwargs) + page_count = math.ceil(issue_count / max_results) if issue_count > 0 else 0 + progress = ChildProgressBuilder.build( + progress_builder, child_total=page_count + 1 + ) + progress.increment(by=1) + try: while True: - url = ( - f"{instance.jira_url}" - + f"/rest/api/2/search" - + f"?startAt={start_at}" - + f"&maxResults={max_results}" + url = f"{instance.jira_url}/rest/api/2/search/jql" + params = { + "jql": jql, + "maxResults": max_results, + "fields": "*all", + } + if next_page_token: + params["nextPageToken"] = next_page_token + + response = advocate.get( + url, headers=headers, params=params, timeout=10, **kwargs ) - if instance.jira_project_key: - url += f"&jql=project={instance.jira_project_key}" - - response = advocate.get(url, headers=headers, timeout=10, **kwargs) if not response.ok: try: json = response.json() @@ -262,25 +294,18 @@ def _fetch_issues(self, instance, progress_builder: ChildProgressBuilder): data = response.json() - # The response of any request gives us the total, allowing us to - # properly construct a progress bar. - if data["total"] and progress is None: - progress = ChildProgressBuilder.build( - progress_builder, - child_total=math.ceil(data["total"] / max_results), - ) - if progress: - progress.increment(by=1) + progress.increment(by=1) - if len(data["issues"]) == 0 and start_at == 0: + if len(data["issues"]) == 0 and next_page_token is None: raise SyncError( "No issues found. This is usually because the authentication " "details are wrong." ) issues.extend(data["issues"]) - start_at += max_results - if data["total"] <= start_at: + + next_page_token = data.get("nextPageToken") + if not next_page_token: break except (RequestException, UnacceptableAddressException, ConnectionError) as e: raise SyncError(f"Error connecting to Jira: {str(e)}") diff --git a/enterprise/backend/tests/baserow_enterprise_tests/data_sync/test_jira_issues_data_sync.py b/enterprise/backend/tests/baserow_enterprise_tests/data_sync/test_jira_issues_data_sync.py index ee75759626..e1bef5d2f4 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/data_sync/test_jira_issues_data_sync.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/data_sync/test_jira_issues_data_sync.py @@ -7,8 +7,7 @@ import pytest import responses -from requests.auth import HTTPBasicAuth -from responses.matchers import header_matcher +from responses.matchers import header_matcher, query_param_matcher from rest_framework.status import HTTP_200_OK, HTTP_402_PAYMENT_REQUIRED from baserow.contrib.database.data_sync.handler import DataSyncHandler @@ -339,39 +338,20 @@ } SINGLE_ISSUE_RESPONSE = { - "expand": "schema,names", - "startAt": 0, - "maxResults": 50, - "total": 1, "issues": [SINGLE_ISSUE], } SINGLE_ISSUE_RESPONSE_PAGE_1 = { - "expand": "schema,names", - "startAt": 0, - "maxResults": 50, - "total": 51, "issues": [SINGLE_ISSUE], + "nextPageToken": "page2token", } SINGLE_ISSUE_RESPONSE_PAGE_2 = { - "expand": "schema,names", - "startAt": 50, - "maxResults": 50, - "total": 51, "issues": [SECOND_ISSUE], } NO_ISSUES_RESPONSE = { - "expand": "schema,names", - "startAt": 0, - "maxResults": 50, - "total": 0, "issues": [], } EMPTY_ISSUE_RESPONSE = { - "expand": "schema,names", - "startAt": 0, - "maxResults": 50, - "total": 1, "issues": [EMPTY_ISSUE], } @@ -440,12 +420,27 @@ def test_sync_data_sync_table(enterprise_data_fixture): encoded_credentials = base64.b64encode(credentials.encode()).decode() auth_header_value = f"Basic {encoded_credentials}" + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=SINGLE_ISSUE_RESPONSE, - match=[header_matcher({"Authorization": auth_header_value})], + match=[ + header_matcher({"Authorization": auth_header_value}), + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -602,12 +597,27 @@ def test_sync_data_sync_table_empty_issue(enterprise_data_fixture): encoded_credentials = base64.b64encode(credentials.encode()).decode() auth_header_value = f"Basic {encoded_credentials}" + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=EMPTY_ISSUE_RESPONSE, - match=[header_matcher({"Authorization": auth_header_value})], + match=[ + header_matcher({"Authorization": auth_header_value}), + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -684,12 +694,27 @@ def test_sync_data_sync_table_empty_issue(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_sync_data_sync_table_personal_access_token(enterprise_data_fixture): + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=EMPTY_ISSUE_RESPONSE, - match=[header_matcher({"Authorization": "Bearer FAKE_PAT"})], + match=[ + header_matcher({"Authorization": "Bearer FAKE_PAT"}), + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -728,20 +753,42 @@ def test_sync_data_sync_table_personal_access_token(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_create_data_sync_table_pagination(enterprise_data_fixture): - basic_auth_header = HTTPBasicAuth("test@test.nl", "test_token") + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 2}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=SINGLE_ISSUE_RESPONSE_PAGE_1, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=50&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=SINGLE_ISSUE_RESPONSE_PAGE_2, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + "nextPageToken": "page2token", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -785,13 +832,26 @@ def test_create_data_sync_table_pagination(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_create_data_sync_table_invalid_auth(enterprise_data_fixture): - basic_auth_header = HTTPBasicAuth("test@test.nl", "test_token") + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 0}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=NO_ISSUES_RESPONSE, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -823,13 +883,26 @@ def test_create_data_sync_table_invalid_auth(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_create_data_sync_table_jira_error_message(enterprise_data_fixture): - basic_auth_header = HTTPBasicAuth("test@test.nl", "test_token") + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=400, json={"errorMessages": ["test error"]}, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -859,13 +932,26 @@ def test_create_data_sync_table_jira_error_message(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_create_data_sync_table_with_project_key(enterprise_data_fixture): - basic_auth_header = HTTPBasicAuth("test@test.nl", "test_token") + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50&jql=project=TEST", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=SINGLE_ISSUE_RESPONSE, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "project=TEST ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() @@ -895,13 +981,26 @@ def test_create_data_sync_table_with_project_key(enterprise_data_fixture): @override_settings(DEBUG=True) @responses.activate def test_create_data_sync_table_jira_not_updated_twice(enterprise_data_fixture): - basic_auth_header = HTTPBasicAuth("test@test.nl", "test_token") + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/search/approximate-count", + status=200, + json={"count": 1}, + ) responses.add( responses.GET, - "https://test.atlassian.net/rest/api/2/search?startAt=0&maxResults=50", + "https://test.atlassian.net/rest/api/2/search/jql", status=200, json=SINGLE_ISSUE_RESPONSE, - headers={"Authorization": f"Basic {basic_auth_header}"}, + match=[ + query_param_matcher( + { + "jql": "created IS NOT EMPTY ORDER BY created DESC", + "maxResults": "50", + "fields": "*all", + } + ), + ], ) enterprise_data_fixture.enable_enterprise() diff --git a/web-frontend/Dockerfile b/web-frontend/Dockerfile index 5e877d0e19..af9ad06b96 100644 --- a/web-frontend/Dockerfile +++ b/web-frontend/Dockerfile @@ -270,15 +270,17 @@ RUN groupadd --system --gid $GID ${DOCKER_USER} && \ useradd --shell /bin/bash -l -u $UID -g $GID -o -c "" -d /baserow -m ${DOCKER_USER} USER $UID:$GID -RUN mkdir -p /baserow/web-frontend +RUN mkdir -p /baserow/web-frontend/docker COPY --chown=$UID:$GID --from=builder-prod /baserow/web-frontend/.output /baserow/web-frontend/.output COPY --chown=$UID:$GID ./web-frontend/env-remap.mjs /baserow/web-frontend/env-remap.mjs +COPY --chown=$UID:$GID ./web-frontend/docker/docker-entrypoint-prod.sh /baserow/web-frontend/docker/docker-entrypoint.sh +RUN chmod a+x /baserow/web-frontend/docker/docker-entrypoint.sh HEALTHCHECK --interval=10s CMD wget -q -O /dev/null http://localhost:3000/_health/ || exit 1 WORKDIR /baserow/web-frontend -CMD ["node", "--import", "./env-remap.mjs", ".output/server/index.mjs"] +ENTRYPOINT ["/baserow/web-frontend/docker/docker-entrypoint.sh"] FROM local AS prod diff --git a/web-frontend/docker/docker-entrypoint-prod.sh b/web-frontend/docker/docker-entrypoint-prod.sh new file mode 100644 index 0000000000..db3e5a4109 --- /dev/null +++ b/web-frontend/docker/docker-entrypoint-prod.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Production entrypoint for the web-frontend container. +# Starts the Nitro/Nuxt production server when no command is given or when a +# known legacy command is used. Otherwise, executes the provided command so the +# image remains usable for one-off commands and debugging. +set -euo pipefail + +start_server() { + exec node --import ./env-remap.mjs .output/server/index.mjs +} + +# No command provided: start the server. +if [[ $# -eq 0 ]]; then + start_server +fi + +# Catch legacy commands that were used in v2.0 and before to start the +# production server (nuxt, nuxt-local) and redirect them to the new Nitro +# server. This ensures backward compatibility with custom docker-compose or +# helm charts that still override the container command. +case "$1" in + nuxt*) + echo "WARNING: legacy command '$1' detected. Starting the Nuxt/Nitro server instead. You can safely remove the 'command' override from your docker-compose file." >&2 + start_server + ;; + *) + exec "$@" + ;; +esac diff --git a/web-frontend/modules/database/components/row/RowHistoryFieldNumber.vue b/web-frontend/modules/database/components/row/RowHistoryFieldNumber.vue index 66b456f842..dacd9d6f66 100644 --- a/web-frontend/modules/database/components/row/RowHistoryFieldNumber.vue +++ b/web-frontend/modules/database/components/row/RowHistoryFieldNumber.vue @@ -14,7 +14,7 @@