Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b491ea5
adding download dataset as a service job
abi-muller Mar 30, 2026
b582bd7
wireframes version one
abi-muller Mar 30, 2026
c96351e
wireframes version 2
abi-muller Mar 31, 2026
cdd272c
wireframes version 3
abi-muller Mar 31, 2026
69c9da7
updated wireframes
abi-muller Apr 9, 2026
f544703
Implement full UI modernisation — replace MUI with custom design system
claude Apr 16, 2026
6086e53
Add tracking files: feature_list.json and claude-progress.md
claude Apr 16, 2026
02a01b5
Mark item 18 as passing: PR raised at https://github.com/no10ds/rapid…
claude Apr 16, 2026
5cca0d1
update frontend packages
abi-muller Apr 17, 2026
42d2228
Merge branch 'fix/last_upoaded_by' into feature/rapid-ui-modernisation
abi-muller Apr 17, 2026
8725f20
fix frontend tests
abi-muller Apr 17, 2026
2f177d7
update frontend
abi-muller Apr 25, 2026
9896a54
fix tests
abi-muller Apr 25, 2026
da3ef63
frontend improvements
abi-muller Apr 27, 2026
7eca0ff
frontend improvements
abi-muller Apr 27, 2026
a84776c
frontend changes
abi-muller Apr 27, 2026
592a736
frontend changes
abi-muller Apr 27, 2026
49352e3
fix frontend tests
abi-muller Apr 27, 2026
89a717a
ui updates
abi-muller May 18, 2026
6b4091f
Merge branch 'main' into feature/rapid-ui-modernisation
abi-muller May 18, 2026
8e3adce
ui updates
abi-muller May 18, 2026
350637c
building re usable MUI components and deleting uneeded code
abi-muller May 21, 2026
d2c143f
cleaning ui code
abi-muller May 22, 2026
acbb399
styling
abi-muller May 22, 2026
5872fe3
update frontend tests
abi-muller Jun 2, 2026
31ce725
update docs
abi-muller Jun 2, 2026
e02984f
ui tests
abi-muller Jun 3, 2026
d7daeb1
ui tests
abi-muller Jun 3, 2026
bef8edc
ui tests
abi-muller Jun 3, 2026
f8cc4e2
ui tests
abi-muller Jun 3, 2026
d40f28f
ui tests
abi-muller Jun 3, 2026
9966979
ui tests
abi-muller Jun 3, 2026
ebfa032
ui tests
abi-muller Jun 3, 2026
a723c2d
remove wireframe
abi-muller Jun 3, 2026
38e8368
ui tests
abi-muller Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- run: pip install -r requirements.txt

Expand Down Expand Up @@ -59,7 +58,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- name: Setup API environment
run: make backend/setup
Expand Down Expand Up @@ -92,7 +90,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- name: Setup Python Environment
run: |
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- run: pip install -r requirements.txt

Expand Down Expand Up @@ -58,7 +57,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- name: Setup API environment
run: make backend/setup
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release_sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
cache: 'pip'

- name: Setup Python Environment
run: |
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ jobs:
E2E_DOMAIN_NAME: ${{ secrets.E2E_DOMAIN_NAME }}
E2E_RESOURCE_PREFIX: ${{ secrets.E2E_RESOURCE_PREFIX }}
AWS_REGION: ${{ secrets.AWS_REGION }}

- name: Upload Playwright results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: |
frontend/test-results/
frontend/playwright-report/
retention-days: 7
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ infra/backend: ## Create terraform backend for infrastructure
@cd infrastructure/; ./scripts/infra_make_helper.sh create_backend

infra/init: ## Terraform init: make infra/init block=<infra/block>
@cd infrastructure/; ./scripts/infra_make_helper.sh run_init "${block}"
@cd infrastructure/; ./scripts/infra_make_helper.sh run_init "${block}" "${args}"

infra/plan: ## Terraform view infrastructure changes: make infra/plan block=<infra/block>
@cd infrastructure/; ./scripts/infra_make_helper.sh run_tf plan "${block}" "${env}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ def get_subject_id(request: Request):
return parsed_token.subject if token else None


def get_username(request: Request) -> Optional[str]:
token = get_token(request)
if not token:
return None
try:
parsed_token = parse_token(token)
except DecodeError:
return None
return parsed_token.username


def check_credentials_availability(
browser_request: bool,
client_token: Optional[str],
Expand Down
17 changes: 17 additions & 0 deletions backend/api/application/services/data_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import uuid
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Thread
from typing import List, Tuple
Expand Down Expand Up @@ -191,6 +192,22 @@ def get_last_updated_time(self, metadata: DatasetMetadata) -> str:
)
return last_updated or "Never updated"

def enrich_datasets_for_ui(self, datasets: List[DatasetMetadata]) -> List[dict]:
def enrich(dataset: DatasetMetadata) -> dict:
d = dataset.to_dict()
try:
d["last_updated"] = self.get_last_updated_time(dataset)
except Exception:
d["last_updated"] = None
try:
d["last_uploaded_by"] = self.get_last_uploader(dataset)
except Exception:
d["last_uploaded_by"] = None
return d

with ThreadPoolExecutor(max_workers=10) as pool:
return list(pool.map(enrich, datasets))

def get_last_uploader(self, metadata: DatasetMetadata) -> str:
"""
Get the name of the user who last successfully uploaded to this dataset.
Expand Down
18 changes: 12 additions & 6 deletions backend/api/controller/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
DatasetAccessEvaluator,
)
from api.application.services.data_service import DataService

from api.application.services.job_service import JobService
from api.application.services.delete_service import DeleteService
from api.application.services.format_service import FormatService
from api.application.services.schema_service import SchemaService
from api.application.services.search_service import SearchService
from api.common.data_handlers import store_file_to_disk
from api.common.utilities import strtobool
from api.common.utilities import build_error_message_list, construct_dataset_metadata, strtobool
from api.common.config.auth import Action
from api.common.config.constants import (
BASE_API_PATH,
Expand All @@ -40,7 +40,6 @@
InvalidFileUploadError,
)
from api.common.logger import AppLogger
from api.common.utilities import construct_dataset_metadata
from api.domain.dataset_filters import DatasetFilters
from api.domain.dataset_metadata import DatasetMetadata
from api.domain.schema_metadata import SchemaMetadata
Expand All @@ -53,6 +52,7 @@

athena_adapter = AthenaAdapter()
data_service = DataService()
job_service = JobService()
delete_service = DeleteService()
schema_service = SchemaService()
data_access_evaluator = DatasetAccessEvaluator()
Expand Down Expand Up @@ -567,9 +567,15 @@ async def query_dataset(
### Click `Try it out` to use the endpoint

"""
df = data_service.query_data(
construct_dataset_metadata(layer, domain, dataset, version), query
)
subject_id = get_subject_id(request)
dataset_metadata = construct_dataset_metadata(layer, domain, dataset, version)
query_job = job_service.create_query_job(subject_id, dataset_metadata)
try:
df = data_service.query_data(dataset_metadata, query)
except Exception as error:
job_service.fail(query_job, build_error_message_list(error))
raise
job_service.succeed_query(query_job, url=None)
Comment thread
abi-muller marked this conversation as resolved.
if df.shape[0] == 0:
# Return 204 if dataframe is empty
return PlainTextResponse(
Expand Down
12 changes: 4 additions & 8 deletions backend/api/domain/schema_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,7 @@ def get_is_latest_version(self) -> bool:
return self.is_latest_version

def remove_duplicates(self):
updated_key_only_list = []

if len(self.key_only_tags) != 0 and self.key_value_tags:
for key in self.key_only_tags:
if key not in self.key_value_tags.keys():
updated_key_only_list.append(key)

self.key_only_tags = updated_key_only_list
if self.key_value_tags:
self.key_only_tags = [
key for key in self.key_only_tags if key not in self.key_value_tags
]
1 change: 1 addition & 0 deletions backend/api/domain/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class Token:
def __init__(self, payload: dict):
self.subject: str = self._extract_subject(payload)
self.username: str | None = payload.get("cognito:username") or payload.get("username")

def _extract_subject(self, payload: dict) -> str:
try:
Expand Down
7 changes: 5 additions & 2 deletions backend/api/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
from api.application.services.authorisation.authorisation_service import (
secure_endpoint,
get_subject_id,
get_username,
)
from api.application.services.permissions_service import PermissionsService
from api.application.services.authorisation.dataset_access_evaluator import (
DatasetAccessEvaluator,
)
from api.application.services.data_service import DataService
from api.common.config.auth import IDENTITY_PROVIDER_BASE_URL, Action
from api.common.config.docs import (
custom_openapi_docs_generator,
Expand Down Expand Up @@ -57,6 +59,7 @@

permissions_service = PermissionsService()
upload_service = DatasetAccessEvaluator()
data_service = DataService()

app = FastAPI(
openapi_url=f"{BASE_API_PATH}/openapi.json", docs_url=None
Expand Down Expand Up @@ -172,7 +175,7 @@ async def methods(request: Request):
except AWSServiceError as error:
error_message = error.message

return {"error_message": error_message, **allowed_actions}
return {"error_message": error_message, "username": get_username(request), **allowed_actions}


@app.get(
Expand All @@ -194,7 +197,7 @@ async def get_permissions_ui():
async def get_datasets_ui(action: Action, request: Request):
subject_id = get_subject_id(request)
datasets = upload_service.get_authorised_datasets(subject_id, action)
return [dataset.to_dict() for dataset in datasets]
return data_service.enrich_datasets_for_ui(datasets)


@app.get("/favicon.ico", include_in_schema=False)
Expand Down
73 changes: 64 additions & 9 deletions backend/test/api/controller/test_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from api.application.services.data_service import DataService
from api.application.services.delete_service import DeleteService
from api.application.services.job_service import JobService
from api.application.services.search_service import SearchService
from api.common.custom_exceptions import (
UserError,
Expand Down Expand Up @@ -819,9 +820,12 @@ def test_returns_error_response_when_domain_uppercase(self):
"details": ["domain -> was required to be lowercase only."]
}

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_call_service_with_only_domain_dataset_when_no_json_provided(
self, mock_query_method
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
query_url = f"{BASE_API_PATH}/datasets/raw/mydomain/mydataset/query?version=1"

Expand All @@ -833,8 +837,13 @@ def test_call_service_with_only_domain_dataset_when_no_json_provided(
DatasetMetadata("raw", "mydomain", "mydataset", 1), Query()
)

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_call_service_with_sql_query_when_json_provided(self, mock_query_method):
def test_call_service_with_sql_query_when_json_provided(
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
request_json = {"select_columns": ["column1"], "limit": "10"}

query_url = f"{BASE_API_PATH}/datasets/raw/mydomain/mydataset/query?version=1"
Expand All @@ -848,10 +857,13 @@ def test_call_service_with_sql_query_when_json_provided(self, mock_query_method)
Query(select_columns=["column1"], limit="10"),
)

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch("api.controller.datasets.construct_dataset_metadata")
@patch.object(DataService, "query_data")
def test_call_service_with_latest_version_when_none_provided(
self, mock_query_method, mock_construct_metadata
self, mock_query_method, mock_construct_metadata, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_construct_metadata.return_value = DatasetMetadata(
"raw", "mydomain", "mydataset", 32
Expand All @@ -864,9 +876,12 @@ def test_call_service_with_latest_version_when_none_provided(
DatasetMetadata("raw", "mydomain", "mydataset", 32), Query()
)

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_calls_service_with_sql_query_when_empty_json_values_provided(
self, mock_query_method
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
request_json = {
"select_columns": ["column1"],
Expand All @@ -891,8 +906,13 @@ def test_calls_service_with_sql_query_when_empty_json_values_provided(
),
)

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_returns_formatted_json_from_query_result(self, mock_query_method):
def test_returns_formatted_json_from_query_result(
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame(
{
"column1": [1, 2],
Expand All @@ -918,8 +938,13 @@ def test_returns_formatted_json_from_query_result(self, mock_query_method):
"1": {"column1": "2", "column2": "item2", "area": "area_2"},
}

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_request_query_in_csv_is_successful(self, mock_query_method):
def test_request_query_in_csv_is_successful(
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame(
{
"column1": [1, 2],
Expand All @@ -937,9 +962,12 @@ def test_request_query_in_csv_is_successful(self, mock_query_method):

assert response.status_code == 200

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_returns_formatted_json_from_query_if_format_is_not_provided(
self, mock_query_method
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame(
{
Expand All @@ -961,8 +989,13 @@ def test_returns_formatted_json_from_query_if_format_is_not_provided(
"1": {"column1": "2", "column2": "item2", "area": "area_2"},
}

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_returns_204_if_dataframe_is_empty(self, mock_query_method):
def test_returns_204_if_dataframe_is_empty(
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame(
{
"column1": [],
Expand All @@ -983,9 +1016,12 @@ def test_returns_204_if_dataframe_is_empty(self, mock_query_method):
== "No rows were returned. Either there is no data or the query is too limiting."
)

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_returns_error_from_query_request_when_format_is_unsupported(
self, mock_query_method
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame(
{
Expand All @@ -1007,6 +1043,25 @@ def test_returns_error_from_query_request_when_format_is_unsupported(
"details": "Provided value for Accept header parameter [text/plain] is not supported. Supported formats: application/json, text/csv, application/octet-stream"
}

@patch.object(JobService, "succeed_query")
@patch.object(JobService, "create_query_job")
@patch("api.controller.datasets.get_subject_id")
@patch.object(DataService, "query_data")
def test_creates_audit_job_on_successful_query(
self, mock_query_method, mock_get_subject_id, mock_create_query_job, mock_succeed_query
):
mock_query_method.return_value = pd.DataFrame({"col": [1]})
mock_get_subject_id.return_value = "subject_id"
fake_job = mock_create_query_job.return_value

query_url = f"{BASE_API_PATH}/datasets/raw/mydomain/mydataset/query?version=1"
self.client.post(query_url, headers={"Authorization": "Bearer test-token"})

mock_create_query_job.assert_called_once_with(
"subject_id", DatasetMetadata("raw", "mydomain", "mydataset", 1)
)
mock_succeed_query.assert_called_once_with(fake_job, url=None)

@pytest.mark.parametrize(
"input_key", ["select_column", "invalid_key", "another_invalid_key"]
)
Expand Down
Loading