-
-
Notifications
You must be signed in to change notification settings - Fork 23
Add data platform toolbox #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
797444e
Add data platform toolbox
katyalmohit 80107b9
Update Data Platform client to use host and port environment variables
katyalmohit 1fcb74d
Simplify response display in data platform toolbox
katyalmohit 8ace063
refactor: split Data Platform toolbox into subsection modules
katyalmohit 019db3e
Use absolute path and add integration test
katyalmohit c0b7a17
Add key to buttons, write tests for organisation and users
katyalmohit 2e926e3
test: add integration tests for organisations, users, locations and p…
katyalmohit 27e241d
Refactor tests: extract repeated gRPC setup into helper functions
katyalmohit 2d81399
Change the functions to async
katyalmohit 1425c2b
Use data-platform enums and write description
katyalmohit f686b67
Remove extra spaces
katyalmohit 99a10f0
Format code with ruff
katyalmohit e767557
Remove table format
katyalmohit 0b1a683
Remove unused import
katyalmohit 9bb91f4
Fix locations UI test
katyalmohit 42fa025
Remove redundant part and rename options
katyalmohit ca42e95
Fix error messages
katyalmohit 66ca071
fix(locations): prevent UNSPECIFIED enum in create/get forms
katyalmohit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| """Location management section for the Data Platform Toolbox""" | ||
|
|
||
| import streamlit as st | ||
| import json | ||
| from dp_sdk.ocf import dp | ||
| from grpclib.exceptions import GRPCError | ||
|
|
||
|
|
||
| async def locations_section(data_client): | ||
| """Location management section.""" | ||
|
|
||
| # Energy source and location type mappings | ||
| ENERGY_SOURCES = { | ||
| "UNSPECIFIED": dp.EnergySource.UNSPECIFIED, | ||
| "SOLAR": dp.EnergySource.SOLAR, | ||
| "WIND": dp.EnergySource.WIND, | ||
| } | ||
|
|
||
| LOCATION_TYPES = { | ||
| "UNSPECIFIED": dp.LocationType.UNSPECIFIED, | ||
| "SITE": dp.LocationType.SITE, | ||
| "GSP": dp.LocationType.GSP, | ||
| "DNO": dp.LocationType.DNO, | ||
| "NATION": dp.LocationType.NATION, | ||
| "STATE": dp.LocationType.STATE, | ||
| "COUNTY": dp.LocationType.COUNTY, | ||
| "CITY": dp.LocationType.CITY, | ||
| "PRIMARY SUBSTATION": dp.LocationType.PRIMARY_SUBSTATION, | ||
| } | ||
|
|
||
| # List Locations | ||
| st.markdown( | ||
| '<h2 style="color:#63BCAF;font-size:32px;">List Locations</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| with st.expander("Filter options"): | ||
| energy_source_filter = st.selectbox( | ||
| "Energy Source", list(ENERGY_SOURCES.keys()), key="list_loc_energy" | ||
| ) | ||
| location_type_filter = st.selectbox( | ||
| "Location Type", list(LOCATION_TYPES.keys()), key="list_loc_type" | ||
| ) | ||
| user_filter = st.text_input( | ||
| "Filter by User OAuth ID (optional)", | ||
| key="list_loc_user", | ||
| help="Leave empty to show all locations", | ||
| ) | ||
| if st.button("List Locations", key="list_locations_button"): | ||
| if not data_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| else: | ||
| try: | ||
| request = dp.ListLocationsRequest() | ||
| if energy_source_filter != "UNSPECIFIED": | ||
| request.energy_source_filter = ENERGY_SOURCES[energy_source_filter] | ||
| if location_type_filter != "UNSPECIFIED": | ||
| request.location_type_filter = LOCATION_TYPES[location_type_filter] | ||
| if user_filter: | ||
| request.user_oauth_id_filter = user_filter | ||
|
|
||
| response = await data_client.list_locations(request) | ||
| locations = response.locations | ||
|
|
||
| if locations: | ||
| st.success(f"✅ Found {len(locations)} location(s)") | ||
| loc_dicts = [loc.to_dict() for loc in locations] | ||
| st.write(loc_dicts) | ||
| else: | ||
| st.info("No locations found with the specified filters") | ||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error listing locations: {str(e)}") | ||
|
|
||
| # Get Location Details | ||
| st.markdown( | ||
| '<h2 style="color:#63BCAF;font-size: 32px;">Get Location Details</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| loc_uuid = st.text_input("Location UUID", key="get_loc_uuid") | ||
| loc_energy = st.selectbox( | ||
| # to avoid defaulting to UNSPECIFIED | ||
| "Energy Source", list(ENERGY_SOURCES.keys())[1:], key="get_loc_energy" | ||
| ) | ||
| include_geometry = st.checkbox("Include Geometry", key="get_loc_geom") | ||
| if st.button("Get Location Details", key="get_location_button"): | ||
| if not loc_uuid.strip(): | ||
| st.warning("⚠️ Please enter a location UUID") | ||
| elif not data_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| else: | ||
| try: | ||
| response = await data_client.get_location( | ||
| dp.GetLocationRequest( | ||
| location_uuid=loc_uuid, | ||
| energy_source=ENERGY_SOURCES[loc_energy], | ||
| include_geometry=include_geometry, | ||
| ) | ||
| ) | ||
| response_dict = response.to_dict() | ||
| st.success(f"✅ Found location: {loc_uuid}") | ||
| st.write(response_dict) | ||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error fetching location: {str(e)}") | ||
|
|
||
| # Create Location | ||
| st.markdown( | ||
| '<h2 style="color:#7bcdf3;font-size:32px;">Create Location</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| with st.expander("Create new location"): | ||
| loc_name = st.text_input("Location Name *", key="create_loc_name") | ||
| loc_energy_src = st.selectbox( | ||
| # to avoid defaulting to UNSPECIFIED | ||
| "Energy Source *", list(ENERGY_SOURCES.keys())[1:], key="create_loc_energy" | ||
| ) | ||
| loc_type = st.selectbox( | ||
| # to avoid defaulting to UNSPECIFIED | ||
| "Location Type *", list(LOCATION_TYPES.keys())[1:], key="create_loc_type" | ||
| ) | ||
| geometry_wkt = st.text_input( | ||
| "Geometry (WKT) *", | ||
| placeholder="POINT(-0.127 51.507)", | ||
| key="create_loc_geom", | ||
| help="Enter location geometry in WKT format (e.g., POINT(lon lat))", | ||
| ) | ||
| capacity_watts = st.number_input( | ||
| "Effective Capacity (Watts) *", | ||
| min_value=0, | ||
| key="create_loc_cap", | ||
| help="Enter the effective capacity in watts", | ||
| ) | ||
| loc_metadata = st.text_area( | ||
| "Metadata (JSON)", | ||
| value="{}", | ||
| key="create_loc_metadata", | ||
| help="Enter valid JSON for location metadata", | ||
| ) | ||
|
|
||
| if st.button("Create Location", key="create_location_button"): | ||
| if not data_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| elif ( | ||
| not loc_name.strip() or not geometry_wkt.strip() or capacity_watts <= 0 | ||
| ): | ||
| st.warning("⚠️ Please fill in all required fields (*)") | ||
| else: | ||
| try: | ||
| # Parse metadata JSON | ||
| metadata = json.loads(loc_metadata) if loc_metadata.strip() else {} | ||
| response = await data_client.create_location( | ||
| dp.CreateLocationRequest( | ||
| location_name=loc_name, | ||
| energy_source=ENERGY_SOURCES.get(loc_energy_src, 1), | ||
| location_type=LOCATION_TYPES.get(loc_type, 1), | ||
| geometry_wkt=geometry_wkt, | ||
| effective_capacity_watts=int(capacity_watts), | ||
| metadata=metadata, | ||
| ) | ||
| ) | ||
| response_dict = response.to_dict() | ||
| st.success(f"✅ Location '{loc_name}' created successfully!") | ||
| st.write(response_dict) | ||
|
|
||
| except json.JSONDecodeError: | ||
| st.error("❌ Invalid JSON in metadata field") | ||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error creating location: {str(e)}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """Data Platform Toolbox Streamlit Page Main Code.""" | ||
|
|
||
| import asyncio | ||
| from grpclib.client import Channel | ||
| import streamlit as st | ||
| from dataplatform.toolbox.organisation import organisation_section | ||
| from dataplatform.toolbox.users import users_section | ||
| from dataplatform.toolbox.user_organisation import user_organisation_section | ||
| from dataplatform.toolbox.location import locations_section | ||
| from dataplatform.toolbox.policy import policies_section | ||
| from dp_sdk.ocf import dp | ||
| import os | ||
|
|
||
| # Color scheme (matching existing toolbox) | ||
| # teal: #63BCAF (Get operations) | ||
| # blue: #7bcdf3 (Create operations) | ||
| # yellow: #ffd053 (Update operations) | ||
| # red: #E63946 (Delete operations) | ||
| # orange: #FF9736 (Info sections) | ||
|
|
||
|
|
||
| def dataplatform_toolbox_page() -> None: | ||
| """Wrapper function that is not async to call the main async function.""" | ||
| asyncio.run(async_dataplatform_toolbox_page()) | ||
|
|
||
|
|
||
| async def async_dataplatform_toolbox_page(): | ||
| """Async Main function for the Data Platform Toolbox Streamlit page.""" | ||
| host = os.environ.get("DATA_PLATFORM_HOST", "localhost") | ||
| port = os.environ.get("DATA_PLATFORM_PORT", "50051") | ||
| async with Channel(host=host, port=int(port)) as channel: | ||
| admin_client = dp.DataPlatformAdministrationServiceStub(channel) | ||
| data_client = dp.DataPlatformDataServiceStub(channel) | ||
|
|
||
| st.markdown( | ||
| '<h1 style="color:#63BCAF;font-size:48px;">Data Platform Toolbox</h1>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
|
|
||
| # Create tabs for different sections | ||
| tab1, tab2, tab3, tab4, tab5 = st.tabs( | ||
| [ | ||
| "🏢 Organisations", | ||
| "👤 Users", | ||
| "🔗 User + Organisation", | ||
| "📍 Locations", | ||
| "📋 Policies", | ||
| ] | ||
| ) | ||
|
|
||
| with tab1: | ||
| await organisation_section(admin_client) | ||
|
|
||
| with tab2: | ||
| await users_section(admin_client) | ||
|
|
||
| with tab3: | ||
| await user_organisation_section(admin_client) | ||
|
|
||
| with tab4: | ||
| await locations_section(data_client) | ||
|
|
||
| with tab5: | ||
| await policies_section(admin_client, data_client) | ||
|
|
||
|
|
||
| # Required for the tests to run this as a script | ||
| if __name__ == "__main__": | ||
| dataplatform_toolbox_page() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| """Organisation management section for the Data Platform Toolbox.""" | ||
|
|
||
| import streamlit as st | ||
| import json | ||
| from dp_sdk.ocf import dp | ||
| from grpclib.exceptions import GRPCError | ||
|
|
||
|
|
||
| async def organisation_section(admin_client): | ||
| """Organisation management section.""" | ||
|
|
||
| # Get Organisation Details | ||
| st.markdown( | ||
| '<h2 style="color:#63BCAF;font-size: 32px;">Get Organisation Details</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| org_name = st.text_input("Organisation Name", key="get_org_name") | ||
| if st.button("Get Organisation Details", key="get_org_button"): | ||
| if not admin_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| elif not org_name.strip(): | ||
| st.warning("⚠️ Please enter an organisation name") | ||
| else: | ||
| try: | ||
| response = await admin_client.get_organisation( | ||
| dp.GetOrganisationRequest(org_name=org_name) | ||
| ) | ||
| response_dict = response.to_dict() | ||
| st.success(f"✅ Found organisation: {org_name}") | ||
| st.write(response_dict) | ||
|
|
||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error fetching organisation: {str(e)}") | ||
|
|
||
| # Create Organisation | ||
| st.markdown( | ||
| '<h2 style="color:#7bcdf3;font-size: 32px;">Create Organisation</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| with st.expander("Create new organisation"): | ||
| new_org_name = st.text_input("New Organisation Name", key="create_org_name") | ||
| metadata_json = st.text_area( | ||
| "Metadata (JSON)", | ||
| value="{}", | ||
| key="create_org_metadata", | ||
| help="Enter valid JSON for organisation metadata", | ||
| ) | ||
|
|
||
| if st.button("Create Organisation", key="create_org_button"): | ||
| if not admin_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| elif not new_org_name.strip(): | ||
| st.warning("⚠️ Please enter an organisation name") | ||
| else: | ||
| try: | ||
| # Parse metadata JSON | ||
| metadata = ( | ||
| json.loads(metadata_json) if metadata_json.strip() else {} | ||
| ) | ||
| response = await admin_client.create_organisation( | ||
| dp.CreateOrganisationRequest( | ||
| org_name=new_org_name, metadata=metadata | ||
| ) | ||
| ) | ||
| response_dict = response.to_dict() | ||
| st.success( | ||
| f"✅ Organisation '{new_org_name}' created successfully!" | ||
| ) | ||
| st.write(response_dict) | ||
|
|
||
| except json.JSONDecodeError: | ||
| st.error("❌ Invalid JSON in metadata field") | ||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error creating organisation: {str(e)}") | ||
|
|
||
| # Delete Organisation | ||
| st.markdown( | ||
| '<h2 style="color:#E63946;font-size:32px;">Delete Organisation</h2>', | ||
| unsafe_allow_html=True, | ||
| ) | ||
| with st.expander("Delete organisation"): | ||
| del_org_name = st.text_input( | ||
| "Organisation Name to Delete", key="delete_org_name" | ||
| ) | ||
| st.warning("⚠️ This action cannot be undone!") | ||
| confirm_delete = st.checkbox( | ||
| "I understand this will permanently delete the organisation", | ||
| key="confirm_delete_org", | ||
| ) | ||
| if st.button("Delete Organisation", key="delete_org_button"): | ||
| if not admin_client: | ||
| st.error("❌ Could not connect to Data Platform") | ||
| elif not del_org_name.strip(): | ||
| st.warning("⚠️ Please enter an organisation name") | ||
| elif not confirm_delete: | ||
| st.warning("⚠️ Please confirm deletion by checking the box above") | ||
| else: | ||
| try: | ||
| await admin_client.delete_organisation( | ||
| dp.DeleteOrganisationRequest(org_name=del_org_name) | ||
| ) | ||
| st.success( | ||
| f"✅ Organisation '{del_org_name}' deleted successfully!" | ||
| ) | ||
|
|
||
| except GRPCError as e: | ||
| st.error( | ||
| f"❌ gRPC Error: {e.message}" | ||
| ) | ||
| except Exception as e: | ||
| st.error(f"❌ Error deleting organisation: {str(e)}") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.