Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fac5b7e
feat: reset tree visual selection state on change tab (Element/Template)
Ellana42 May 5, 2026
65866c7
Merge branch 'feature/af-tree-explorer-recipe' into feature/dss14-sc-…
Ellana42 May 5, 2026
3e34dca
feat: attribute search (empty sections still show)
Ellana42 May 9, 2026
64348a2
refacto: improve naming and performance comment
Ellana42 May 9, 2026
00803bf
feat: hide section (template group for ex) if no search match in them
Ellana42 May 9, 2026
4ff9c8e
feat: empty state when all attrbutes have been searched out
Ellana42 May 9, 2026
39e6644
feat: removed max count from aggregates
Ellana42 May 9, 2026
4ecfa9b
feat: reset attribute search on template change
Ellana42 May 9, 2026
41b40e3
feat: hide filter by category dropdowns temporarily
Ellana42 May 9, 2026
453a83c
feat: add missing aggregate types + removed dependsOn from the data
Ellana42 May 11, 2026
a652d74
adding get attribute from template endpoint
alexbourret May 11, 2026
13ce567
fix
alexbourret May 11, 2026
201a463
add get_elements_for_template endpoint
alexbourret May 11, 2026
c47e7eb
feat: load template attributes as children
Ellana42 May 11, 2026
9a1486d
feat: display template attributes in the right table
Ellana42 May 11, 2026
d7b7fd4
feat: don't display element table in template mode
Ellana42 May 11, 2026
d9a2d1d
comment
Ellana42 May 11, 2026
ea4de2f
feat: untoggle templates in template mode (alignment with element mode)
Ellana42 May 11, 2026
4a73be3
feat: temporary checkbox to display the path column
Ellana42 May 11, 2026
2279699
fix for attributes' template names
alexbourret May 12, 2026
95a0489
feat: basic element dropdown functionality + moved onNodeClick to main
Ellana42 May 13, 2026
a2cb946
feat: elements dropdown refreshes on tree interaction
Ellana42 May 13, 2026
ca6c395
feat: improve empty element dropdown state
Ellana42 May 13, 2026
72dabf8
refacto: CLEANUP attribute and template implications in element search
Ellana42 May 13, 2026
0c39773
cleanup: removed ununsed functions
Ellana42 May 15, 2026
459a556
TEMP FIX: stop inserting attributes in tree when search
Ellana42 May 15, 2026
bc8e95a
TEMP FIX: expand nodes in matched search element path
Ellana42 May 15, 2026
307461c
TEMP FIX: load siblings when opening new nodes with search
Ellana42 May 15, 2026
ea8b208
feat: remove start and end time for aggregates
Ellana42 May 15, 2026
5368171
feat: add interval and sync time to output
Ellana42 May 15, 2026
74588f5
cleanup: unused functions and variables
Ellana42 May 15, 2026
68ed21e
feat: clear search button for element search
Ellana42 May 18, 2026
70388db
fix: init element dropdown on reload
Ellana42 May 18, 2026
1af10c4
feat: attribute search matches attribute description as well
Ellana42 May 18, 2026
1cb109c
AF Hierarchy connector does not retrieve elements
alexbourret May 18, 2026
e4b7cc7
fix: search attribute reacts on text input change, not keyboard events
Ellana42 May 18, 2026
caebed6
fix: handle properly attribute description when searching
Ellana42 May 18, 2026
436e6e3
feat: display number of attributes shown
Ellana42 May 18, 2026
4785529
feat: checkbox status changes based on search
Ellana42 May 18, 2026
45c77c6
feat: TMP CSS active state for template/element tabs
Ellana42 May 18, 2026
73bc039
cleanup: removed old template commented code
Ellana42 May 18, 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
2 changes: 2 additions & 0 deletions custom-recipes/pi-system-af-tree/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def next_tree_item(tree_data):
{'name': 'record_boundary_type', 'type': 'string'},
{'name': 'summary_duration', 'type': 'string'},
{'name': 'max_count', 'type': 'int'},
{'name': 'interval', 'type': 'string'},
{'name': 'sync_time', 'type': 'string'},

]
output_dataset.write_schema(schema)
Expand Down
557 changes: 238 additions & 319 deletions js/pi-system_treecontroller.js

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions python-connectors/pi-system_hierarchy/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ def recurse_next_item(self, next_url, parent=None, type=None):
return
for item in items:
parent_path = item.get("Path")
link_to_attributes = self.client.extract_link_with_key(item, "Attributes")
if link_to_attributes:
for attribute in self.recurse_next_item(link_to_attributes, parent=parent_path, type="Attribute"):
yield attribute
# link_to_attributes = self.client.extract_link_with_key(item, "Attributes")
# if link_to_attributes:
# for attribute in self.recurse_next_item(link_to_attributes, parent=parent_path, type="Attribute"):
# yield attribute
link_to_elements = self.client.extract_link_with_key(item, "Elements")
if link_to_elements:
for element in self.recurse_next_item(link_to_elements, parent=parent_path, type="Element"):
Expand Down Expand Up @@ -134,7 +134,7 @@ def batch_next_item(self, next_item, parent=None, type=None):
for retrieved_item in retrieved_items:
retrieved_item_path = retrieved_item.get("Path")
elements_url = self.client.extract_link_with_key(retrieved_item, "Elements")
attributes_url = self.client.extract_link_with_key(retrieved_item, "Attributes")
# attributes_url = self.client.extract_link_with_key(retrieved_item, "Attributes")
if elements_url:
todo_list.append(
{
Expand All @@ -143,14 +143,14 @@ def batch_next_item(self, next_item, parent=None, type=None):
"parent": parent_of_batched_item + "\\" + retrieved_item.get("Name")
}
)
if attributes_url:
todo_list.append(
{
"url": attributes_url,
"type": "Attribute",
"parent": parent_of_batched_item + "\\" + retrieved_item.get("Name")
}
)
# if attributes_url:
# todo_list.append(
# {
# "url": attributes_url,
# "type": "Attribute",
# "parent": parent_of_batched_item + "\\" + retrieved_item.get("Name")
# }
# )
yield {
"ItemType": type,
"Name": retrieved_item.get("Name"),
Expand Down
5 changes: 3 additions & 2 deletions python-lib/osisoft_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,14 +758,15 @@ def search_attributes(self, database_webid, **kwargs):
else:
json_response = None

def search_elements(self, database_webid, name=None, description=None, category=None, template=None, full_search=True):
def search_elements(self, database, name=None, description=None, category=None, template=None, full_search=True):
headers = self.get_requests_headers()
tempo_maxcount = OSIsoftConstants.DEFAULT_MAXCOUNT
params = {
"maxCount": tempo_maxcount,
"associations": "Paths",
}
url = self.endpoint.get_base_url() + "/assetdatabases/{}/elements".format(database_webid)
# url = self.endpoint.get_base_url() + "/assetdatabases/{}/elements".format(database_webid)
url = "{}/elements".format(database)
if name:
params["nameFilter"] = name
if description:
Expand Down
3 changes: 2 additions & 1 deletion python-lib/osisoft_plugin_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,8 +748,9 @@ def recursive_tree_rebuild(dictionary, records, counter=None):
children = []
# context["id"] = str(counter)
context["title"] = key
context["expanded"] = True
# context["expanded"] = True
# context["checked"] = False
context["children"] = children
# logger.info("context post" + str(context))
output.append(context)
return output
2 changes: 1 addition & 1 deletion resource/attribute-table-row.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</td>
<td>{{ctrl.mergedAttribute.title}}</td>
<td>{{ctrl.mergedAttribute.description}}</td>
<td>
<td ng-if="ctrl.displayPath">
<span ng-repeat="path in ctrl.mergedAttribute.paths">
{{path}}<br/>
</span>
Expand Down
144 changes: 125 additions & 19 deletions resource/browse_af_tree.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
from osisoft_client import OSIsoftClient
from safe_logger import SafeLogger
from osisoft_plugin_common import get_credentials, build_select_choices, check_debug_mode
Expand Down Expand Up @@ -77,6 +78,7 @@ def do(payload, config, plugin_config, inputs):
)

method = payload.get("method")
logger.info("Running do for method '{}'".format(method))
if method == "get_query_catalogs":
return get_query_catalogs(None, config)
if method == "get_children_from_db":
Expand Down Expand Up @@ -107,6 +109,34 @@ def do(payload, config, plugin_config, inputs):
"get_element_categories_from_db",
lambda: get_items_from_db(client, parent, "ElementCategories", database_name=database_name)
)
if method == "get_elements_for_template":
database_name = config.get("database_name")
template_name = payload.get("template_name", None)
elements = []
for element in client.search_elements(database_name, name=None, description=None, category=None, template=template_name, full_search=True):
elements.append(get_item_details(element))
return {"choices": [], "elements": elements}
if method=="get_attribute_for_template":
database_name = config.get("database_name")
template_name = payload.get("template_name", None)
if template_name is None:
return {"choices": [], "attributes": []}
# when searching for template : attribute_name=None, element_category=None, attribute_category=None
elements_max_count, attributes_max_count = get_max_counts(config)
attributes = []
for result in client.batched_search(
database_name, None, None,
None, None, template_name, [],
elements_max_count=elements_max_count, attributes_max_count=attributes_max_count
):
attributes.append(result)
attributes = split_real_from_linked_paths(attributes)
items = []
for attribute in attributes:
item = get_item_details(attribute)
items.append(item)
items = expand_items_by_paths(items)
return {"choices": [], "attributes": items}
if method == "do_search":
template_name = config.get("template", None)
category_name = config.get("element_category", None)
Expand All @@ -133,6 +163,7 @@ def do(payload, config, plugin_config, inputs):
attribute_category = None
database_name = config.get("database_name")
element_name = config.get("element_name")
# TODO: remove, stale
attribute_name = config.get("attribute_name")
if isinstance(element_name, str):
element_name = element_name.strip()
Expand All @@ -144,6 +175,7 @@ def do(payload, config, plugin_config, inputs):
attribute_name = None

has_attribute_filter = attribute_name is not None
# TODO: remove, never true anymore
is_template_tab = active_tab == "template"
has_clicked_element_nodes = len(clicked_nodes) > 0
# clicked_nodes scope is only for element-node URLs (batched_search restrict_to_elements).
Expand Down Expand Up @@ -177,7 +209,7 @@ def do(payload, config, plugin_config, inputs):
clicked_nodes = []
# root_tree = payload.get("root_tree")
root_tree = config.get("treeData", [])
root_tree = shorten_tree(root_tree)
root_tree_before_search = copy.deepcopy(root_tree)
attributes = []
# https://dku-qa-osi.francecentral.cloudapp.azure.com/piwebapi/assetdatabases/F1RD3VEt1yTvt0ip6-a5yeEVsgbMcrwu_Je0qg9btcZIvPswT1NJU09GVC1QSS1TRVJWXFdFTEw
database_webid = database_name.split("/")[-1]
Expand Down Expand Up @@ -208,6 +240,7 @@ def do(payload, config, plugin_config, inputs):
items = expand_items_by_paths(items)
attributesCopy = [dict(item) for item in items]
rebuilt_tree = rebuild_tree(client, items.copy(), root_tree)
expand_nodes_for_matched_paths(client, rebuilt_tree, items, root_tree_before_search)
logger.info("Search network timer:{}".format(network_timer.get_report()))
return {"choices": rebuilt_tree, "attributes": attributesCopy}

Expand Down Expand Up @@ -273,14 +306,6 @@ def get_children_from_db(client, parent_node, database_name=None):
attributes_url = links.get("Attributes")
elements_url = links.get("Elements")
children = []
if elements_url:
elements = client.get_next_item_from_url(elements_url, params={"associations": "Paths"})
for element in elements:
child = get_item_details(element)
# child["title"] = "🧩{}".format(child.get("title"))
child["type"] = "element"
child["children"] = []
children.append(child)
if attributes_url:
attributes = client.get_next_item_from_url(attributes_url, params={"associations": "Paths"})
templates_urls = []
Expand All @@ -298,6 +323,14 @@ def get_children_from_db(client, parent_node, database_name=None):
for child, template_name in zip(children, templates_names):
if template_name:
child["template_name"] = template_name
if elements_url:
elements = client.get_next_item_from_url(elements_url, params={"associations": "Paths"})
for element in elements:
child = get_item_details(element)
# child["title"] = "🧩{}".format(child.get("title"))
child["type"] = "element"
child["children"] = []
children.append(child)
return {"choices": children}


Expand Down Expand Up @@ -346,18 +379,79 @@ def nest_children(items):
def rebuild_tree(client, items, root_tree=None):
# builds an active tree containing all the items and their parent up to the root
tree = Tree(root_tree=root_tree)
# tree.print()
while items:
item = items.pop()
if item is None:
break
find_all_ancestors(client, item, tree)
update_item(item, tree)
continue
find_missing_element_ancestors(client, item, tree)
if is_attribute_item(item):
continue
insert_missing_element(item, tree)
result = recursive_tree_rebuild(tree.get_tree(), tree.get_records())
result = drop_first_levels(result)
return result


def expand_nodes_for_matched_paths(client, tree, items, root_tree):
if not isinstance(tree, list) or not isinstance(items, list):
return

for item in items:
item_path = item.get("path")
element_tokens, attribute_tokens = path_to_list(item_path)
if not element_tokens or not attribute_tokens:
continue
mark_expanded_path(client, tree, element_tokens[2:], root_tree)


def mark_expanded_path(client, nodes, path_tokens, root_tree):
if not isinstance(nodes, list) or not path_tokens:
return

current_nodes = nodes
traversed_tokens = []
for token in path_tokens:
matching_node = None
for node in current_nodes:
if node.get("title") == token:
matching_node = node
break
if matching_node is None:
return
traversed_tokens.append(token)
was_expanded = bool(matching_node.get("expanded"))
matching_node["expanded"] = True
previous_node = find_node_by_path_tokens(root_tree, traversed_tokens)
if (
not was_expanded and
(
previous_node is None or
not isinstance(previous_node.get("children"), list) or
len(previous_node.get("children")) == 0
)
):
matching_node["children"] = get_children_from_db(client, matching_node).get("choices", [])
current_nodes = matching_node.get("children", [])


def find_node_by_path_tokens(nodes, path_tokens):
if not isinstance(nodes, list) or not path_tokens:
return None

current_nodes = nodes
current_node = None
for token in path_tokens:
current_node = None
for node in current_nodes:
if node.get("title") == token:
current_node = node
break
if current_node is None:
return None
current_nodes = current_node.get("children", [])
return current_node


def drop_first_levels(result):
# recursively removes the 2 first levels of the returned tree
# (server and DB)
Expand All @@ -373,10 +467,12 @@ def drop_first_levels(result):
return output_result


def find_all_ancestors(client, item, tree):
# Find all the ancestors of an item
def find_missing_element_ancestors(client, item, tree):
# Find the missing element ancestors of an item without loading attributes.
elements_paths_tokens, attributes_paths_tokens = path_to_list(item.get("path"))
client.traverse_and_cache(elements_paths_tokens, attributes_paths_tokens, tree)
if not elements_paths_tokens:
return
client.traverse_and_cache(elements_paths_tokens, [], tree)


def combine_trees(final_tree, all_item_s_ancestors):
Expand Down Expand Up @@ -448,11 +544,21 @@ def set_as_selected(items):
return items


def update_item(item, tree):
def is_attribute_item(item):
if not isinstance(item, dict):
return False
if item.get("type") == "attribute":
return True
return bool(item.get("path")) and "|" in item.get("path", "")


def insert_missing_element(item, tree):
elements_paths_tokens, attributes_paths_tokens = path_to_list(item.get("path"))
if not elements_paths_tokens and not attributes_paths_tokens:
if not elements_paths_tokens or attributes_paths_tokens:
return
if tree.exists(elements_paths_tokens):
return
tree.put(elements_paths_tokens + attributes_paths_tokens, item)
tree.put(elements_paths_tokens, item)


def get_max_counts(config):
Expand Down
Loading