Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be4cdd8
Add asset copy functionality with tabbed interface in asset creation …
joshuaunity May 22, 2026
f5aa38b
feat: Enhance asset copy tab with infinite scroll and loading indicators
joshuaunity May 22, 2026
d2c94fe
Update flexmeasures/ui/templates/assets/asset_new.html
joshuaunity May 25, 2026
609425b
feat: Add asset type filtering to asset API and update UI parameters
joshuaunity May 25, 2026
3336c0c
chore: add changelog entry
joshuaunity May 25, 2026
a1c3380
ref: make card title clickable
joshuaunity May 25, 2026
28329b9
feat: Set external_id to None in copy_asset to prevent unique constra…
joshuaunity May 25, 2026
4b472a4
chore: code linting
joshuaunity May 25, 2026
339f8ae
feat: Remove unused parentAssetId variable and related logic from ass…
joshuaunity May 25, 2026
967b378
Merge branch 'main' into feat/asset-create-copy-tab-ui-flow
joshuaunity May 29, 2026
5f4838f
feat: Update asset type handling and add AssetTypeIdField for better …
joshuaunity May 29, 2026
6573504
chore: code formatting
joshuaunity May 29, 2026
e5749e3
feat: Add parent asset ID handling for asset copy functionality
joshuaunity Jun 1, 2026
14b2902
feat: Set external_id to None when copying assets to avoid unique con…
joshuaunity Jun 1, 2026
b407e22
Update flexmeasures/ui/templates/assets/asset_new.html
joshuaunity Jun 2, 2026
5ce1008
fix: fixed issue were public assets are always added
joshuaunity Jun 2, 2026
5f3bdc2
feat: enhance asset copy UI with pagination and improved loading state
joshuaunity Jun 2, 2026
8a94c30
feat: update asset copy functionality to handle None values for accou…
joshuaunity Jun 12, 2026
26a042f
chore; set search input to search template keyword by default
joshuaunity Jun 12, 2026
e022ca4
Merge branch 'main' into feat/asset-create-copy-tab-ui-flow
nhoening Jul 1, 2026
75f242f
use all_accessible param to look up assets, show 15 per page
nhoening Jul 1, 2026
80b993f
add a doc section on how to create a child asset
nhoening Jul 1, 2026
25c75a1
no pre-fill of search term, let's do that in the next PR
nhoening Jul 1, 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
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ New features
* New ``GET /api/v3_0/sources`` endpoint to list accessible data sources and defined types, with ``only_latest=true`` by default to return only the most recent version per source [see `PR #2126 <https://www.github.com/FlexMeasures/flexmeasures/pull/2126>`_]
* Add support for filtering sensor data GET requests by ``source-type`` on ``/api/v3_0/sensors/<id>/data`` [see `PR #2127 <https://www.github.com/FlexMeasures/flexmeasures/pull/2127>`_]
* Making monitoring alerts more flexible: allow ``flexmeasures monitor`` alerts to target one or more user IDs or email addresses with ``--recipient``; ``flexmeasures monitor last-seen`` can now narrow monitored users to one or more accounts with ``--account`` or to client accounts with ``--consultancy`` [see `PR #2158 <https://www.github.com/FlexMeasures/flexmeasures/pull/2158>`_]
* Support for creating new assets by using another asset as a template from the UI. [see `PR #2195 <https://www.github.com/FlexMeasures/flexmeasures/pull/2195>`_]
* Improve LightGBM daily seasonal lag handling for sub-hourly forecasting sensors [see `PR #2157 <https://www.github.com/FlexMeasures/flexmeasures/pull/2157>`_]

Infrastructure / Support
Expand Down
73 changes: 48 additions & 25 deletions documentation/views/asset-data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,27 @@ Per asset, you can set fields in :ref:`the flex-context <flex_context>`, which w

|

Editing an asset's flex-model

Creating a new child asset
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Per asset, you can set fields in :ref:`the flex-model <flex_model>`. The flex model dialogue allows you to define a set or a single value type based on what the field allows, these include fixed values, sensors, floats, list (sensors or fixed values) and booleans. Initially, no fields are set.
From the context page, you can create a new child asset (you can also get there from the properties page, actually).
There are to ways to do that.

.. image:: https://github.com/FlexMeasures/screenshots/raw/main/screenshot_asset_flexmodel.png
A simple asset form, including a map selector:

.. image:: https://github.com/FlexMeasures/screenshots/raw/main/screenshot_asset_new.png
:align: center
.. :scale: 40%

|

Flex context overview
"""""""""""""""""""""""

* **Left Panel:** Displays a list of currently configured fields.
* **Right Panel:** Shows details of the selected field and provides a form to modify its value.

Adding a field
"""""""""""""""
1. **Select Field:** Choose the desired field from the dropdown menu in the top right corner of the modal.
2. **Add Field:** Click the "Add Field" button next to the dropdown.
3. The field will be added to the list in the left panel.

Setting a field value
"""""""""""""""""""""

1. **Select Field (if it is not selected yet):** Click on the field in the left panel.
2. **Save Value:** In the right panel, use the provided form to set the field's value.
You can also copy an exisiting asset, including its child assets, sensors and flex-config.
You can search for the asset you want to copy:

* Some fields may only accept a sensor value.
* Other fields may accept either a sensor or a fixed value.
.. image:: https://github.com/FlexMeasures/screenshots/raw/main/screenshot_asset_copy.png
:align: center
.. :scale: 40%

|

Expand Down Expand Up @@ -161,9 +151,9 @@ An example would be:
Properties page
---------------

The properties page allows you to view and edit the properties of the asset.
It lists stored secret names without exposing their values and shows a
human-readable expiration time when one is available.
The properties page allows you to view and edit the properties of the asset,
including attributes, sensors and child assets.
Also, it lists stored secret names and expiration times without exposing their values.

You can also delete the asset by clicking on the "Delete this asset" button.

Expand All @@ -173,6 +163,39 @@ You can also delete the asset by clicking on the "Delete this asset" button.

|

Editing an asset's flex-model
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Per asset, you can set fields in :ref:`the flex-model <flex_model>`. The flex model dialogue allows you to define a set or a single value type based on what the field allows, these include fixed values, sensors, floats, list (sensors or fixed values) and booleans. Initially, no fields are set.

.. image:: https://github.com/FlexMeasures/screenshots/raw/main/screenshot_asset_flexmodel.png
:align: center
.. :scale: 40%

|

Flex context overview
"""""""""""""""""""""""

* **Left Panel:** Displays a list of currently configured fields.
* **Right Panel:** Shows details of the selected field and provides a form to modify its value.

Adding a field
"""""""""""""""
1. **Select Field:** Choose the desired field from the dropdown menu in the top right corner of the modal.
2. **Add Field:** Click the "Add Field" button next to the dropdown.
3. The field will be added to the list in the left panel.

Setting a field value
"""""""""""""""""""""

1. **Select Field (if it is not selected yet):** Click on the field in the left panel.
2. **Save Value:** In the right panel, use the provided form to set the field's value.

* Some fields may only accept a sensor value.
* Other fields may accept either a sensor or a fixed value.

|
.. _view_asset_status:

Status page
Expand Down
13 changes: 12 additions & 1 deletion flexmeasures/api/common/schemas/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from flexmeasures.api.common.schemas.generic_schemas import PaginationSchema
from flexmeasures.api.common.schemas.users import AccountIdField
from flexmeasures.data.schemas import AssetIdField
from flexmeasures.data.schemas.generic_assets import GenericAssetSchema
from flexmeasures.data.schemas.generic_assets import (
AssetTypeIdField,
GenericAssetSchema,
)


default_response_fields = ["id", "name", "account_id", "generic_asset_type"]
Expand Down Expand Up @@ -92,6 +95,14 @@ class AssetAPIQuerySchema(PaginationSchema):
example=False,
),
)
asset_type = AssetTypeIdField(
data_key="asset_type",
required=False,
metadata=dict(
description="Filter assets by generic asset type ID.",
example=2,
),
)


class PublicAssetAPISchema(Schema):
Expand Down
16 changes: 12 additions & 4 deletions flexmeasures/api/common/utils/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def _update_sensor_refs_in_subtree(

def _copy_asset_subtree(
source_asset: GenericAsset,
destination_account_id: int,
destination_account_id: int | None,
destination_parent_asset_id: int | None,
asset_schema: AssetSchema,
add_copy_suffix: bool,
Expand All @@ -462,6 +462,8 @@ def _copy_asset_subtree(
)
asset_kwargs["account_id"] = destination_account_id
asset_kwargs["parent_asset_id"] = destination_parent_asset_id
# set external_id to None to avoid conflicts with unique constraint on (account_id, external_id)
asset_kwargs["external_id"] = None
asset_kwargs = convert_asset_json_fields(asset_kwargs)

copied_asset = GenericAsset(**asset_kwargs)
Expand Down Expand Up @@ -490,7 +492,7 @@ def _copy_asset_subtree(

def _determine_copy_name(
source_name: str,
destination_account_id: int,
destination_account_id: int | None,
destination_parent_asset_id: int | None,
) -> str:
"""Return the next available copy name for the destination context.
Expand Down Expand Up @@ -581,13 +583,19 @@ def copy_asset(
asset_schema = AssetSchema()

if account is None and parent_asset is None:
target_account_id = int(asset.account_id)
target_account_id = (
int(asset.account_id) if asset.account_id is not None else None
)
target_parent_asset_id = asset.parent_asset_id
elif account is not None and parent_asset is None:
target_account_id = int(account.id)
target_parent_asset_id = None
elif account is None and parent_asset is not None:
target_account_id = int(parent_asset.account_id)
target_account_id = (
int(parent_asset.account_id)
if parent_asset.account_id is not None
else None
)
target_parent_asset_id = int(parent_asset.id)
else:
target_account_id = int(account.id)
Expand Down
18 changes: 17 additions & 1 deletion flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def index(
fields_in_response: list[str] | None,
all_accessible: bool,
include_public: bool,
asset_type: GenericAssetType | None = None,
account: Account | None = None,
root_asset: GenericAsset | None = None,
max_depth: int | None = None,
Expand All @@ -295,6 +296,7 @@ def index(
- The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.
- Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.
- The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.
- The `asset_type` query parameter can be used to filter by generic asset type ID.
- The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).
- The `depth` query parameter can be used to search only a max number of descendant generations from the root.

Expand Down Expand Up @@ -371,6 +373,10 @@ def index(
filter_statement = GenericAsset.account_id.in_(account_ids)
if include_public:
filter_statement = filter_statement | GenericAsset.account_id.is_(None)
if asset_type is not None:
filter_statement = filter_statement & (
GenericAsset.generic_asset_type_id == asset_type.id
)

query = query_assets_by_search_terms(
search_terms=filter,
Expand Down Expand Up @@ -1784,10 +1790,20 @@ def copy_assets(
# permission is sufficient (any account member may add children to an asset).
# When creating a top-level asset (no parent), we fall back to the account-level
# create-children check, which requires account-admin or consultant.
# Special case: public-to-public copies (both None) require site admin.
if resolved_parent is not None:
check_access(resolved_parent, "create-children")
else:
elif resolved_account is not None:
check_access(resolved_account, "create-children")
else:
# Public asset copy (account_id=None, parent_asset_id=None).
# Only site admins may create public assets.
if not running_as_cli() and not user_has_admin_access(
current_user, "update"
):
raise Forbidden(
"Only site admins may create public assets (account_id=None)."
)

try:
new_asset = copy_asset(asset, account=account, parent_asset=parent_asset)
Expand Down
31 changes: 31 additions & 0 deletions flexmeasures/api/v3_0/tests/test_assets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,37 @@ def test_get_assets(
assert turbine["account_id"] == setup_accounts["Supplier"].id


@pytest.mark.parametrize("requesting_user", ["test_admin_user@seita.nl"], indirect=True)
def test_get_assets_filtered_by_asset_type(
client, setup_api_test_data, setup_accounts, requesting_user
):
supplier_account = setup_accounts["Supplier"]
supplier_account_id = supplier_account.id
supplier_assets = supplier_account.generic_assets

requested_type_id = supplier_assets[0].generic_asset_type_id
expected_assets = [
asset
for asset in supplier_assets
if asset.generic_asset_type_id == requested_type_id
]

response = client.get(
url_for("AssetAPI:index"),
query_string={
"account_id": supplier_account_id,
"asset_type": requested_type_id,
},
)

assert response.status_code == 200
assert len(response.json) == len(expected_assets)
assert all(
asset["generic_asset_type"]["id"] == requested_type_id
for asset in response.json
)


@pytest.mark.parametrize(
"requesting_user, sort_by, sort_dir, expected_name_of_first_sensor",
[
Expand Down
18 changes: 18 additions & 0 deletions flexmeasures/data/schemas/generic_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,24 @@ class Meta:
model = GenericAssetType


class AssetTypeIdField(MarshmallowClickMixin, fields.Int):
"""Field that deserializes to a GenericAssetType and serializes back to an integer."""

def _deserialize(self, value: Any, attr, data, **kwargs) -> GenericAssetType:
"""Turn a generic asset type id into a GenericAssetType."""
asset_type_id: int = super()._deserialize(value, attr, data, **kwargs)
asset_type = db.session.get(GenericAssetType, asset_type_id)
if asset_type is None:
raise FMValidationError(
f"No generic asset type found with id {asset_type_id}."
)
return asset_type

def _serialize(self, value: GenericAssetType, attr, obj, **kwargs):
"""Turn a GenericAssetType into a generic asset type id."""
return value.id


class GenericAssetIdField(MarshmallowClickMixin, fields.Int):
"""Field that deserializes to a GenericAsset and serializes back to an integer."""

Expand Down
17 changes: 16 additions & 1 deletion flexmeasures/ui/static/openapi-specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3477,7 +3477,7 @@
"/api/v3_0/assets": {
"get": {
"summary": "List assets accessible by the user.",
"description": "This endpoint returns all assets that are accessible by the user after applying optional filters.\n\n - The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.\n - Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.\n - The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.\n - The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).\n - The `depth` query parameter can be used to search only a max number of descendant generations from the root.\n\nThe endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.\n - If the `page` parameter is not provided, all assets are returned, without pagination information. The result will be a list of assets.\n - If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).\n - If a search 'filter' such as 'solar \"ACME corp\"' is provided, the response will return only assets where each search term is either present in their name or account name, or is a prefix of their ID.\n The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)\n\nPer default, the response only includes a limited set of asset fields (id, name, account_id, generic_asset_type).\nYou can use the `fields` query parameter to specify a custom set of fields to include in the response.\n",
"description": "This endpoint returns all assets that are accessible by the user after applying optional filters.\n\n - The `account_id` query parameter can be used to list assets from any account (if the user is allowed to read them). Per default, the user's account is used.\n - Alternatively, the `all_accessible` query parameter can be used to list assets from all accounts the current_user has read-access to, plus all public assets. Defaults to `false`.\n - The `include_public` query parameter can be used to include public assets in the response. Defaults to `false`.\n - The `asset_type` query parameter can be used to filter by generic asset type ID.\n - The `root` query parameter can be used to list only descendants of a given root asset (including the root itself).\n - The `depth` query parameter can be used to search only a max number of descendant generations from the root.\n\nThe endpoint supports pagination of the asset list using the `page` and `per_page` query parameters.\n - If the `page` parameter is not provided, all assets are returned, without pagination information. The result will be a list of assets.\n - If a `page` parameter is provided, the response will be paginated, showing a specific number of assets per page as defined by `per_page` (default is 10).\n - If a search 'filter' such as 'solar \"ACME corp\"' is provided, the response will return only assets where each search term is either present in their name or account name, or is a prefix of their ID.\n The response schema for pagination is inspired by [DataTables](https://datatables.net/manual/server-side#Returned-data)\n\nPer default, the response only includes a limited set of asset fields (id, name, account_id, generic_asset_type).\nYou can use the `fields` query parameter to specify a custom set of fields to include in the response.\n",
"security": [
{
"ApiKeyAuth": []
Expand Down Expand Up @@ -3602,6 +3602,16 @@
"example": false
},
"required": false
},
{
"in": "query",
"name": "asset_type",
"description": "Filter assets by generic asset type ID.",
"schema": {
"type": "integer",
"example": 2
},
"required": false
}
],
"responses": {
Expand Down Expand Up @@ -4910,6 +4920,11 @@
"default": false,
"description": "Whether to include public assets. Ignored if an `account_id` is set. To fetch only public assets, use [/assets/public/](#/Assets/get_api_v3_0_assets_public) instead.",
"example": false
},
"asset_type": {
"type": "integer",
"description": "Filter assets by generic asset type ID.",
"example": 2
}
},
"additionalProperties": false
Expand Down
Loading
Loading