diff --git a/src/TenantTabContent.tsx b/src/TenantTabContent.tsx
index 95e9be8..8ac0ed9 100644
--- a/src/TenantTabContent.tsx
+++ b/src/TenantTabContent.tsx
@@ -2,7 +2,13 @@ import React, { FC, useState, useEffect } from 'react';
import { Box, Typography } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { TenantTabTarget, UpdateTenantTabSelectionFn } from './tenantTab';
-import { fetchDatabases, fetchTables, fetchSchema } from './api';
+import {
+ fetchDatabases,
+ fetchTables,
+ fetchSchema,
+ fetchTenantDetail,
+ ITenantDetailResponse
+} from './api';
interface ITenantTabContentProps {
target: TenantTabTarget;
@@ -44,7 +50,9 @@ const tableStyles = {
borderBottom: '1px solid var(--jp-border-color2)',
fontWeight: 600,
fontSize: '12px',
- color: 'var(--jp-ui-font-color0)'
+ color: 'var(--jp-ui-font-color2)',
+ textTransform: 'uppercase' as const,
+ letterSpacing: '0.5px'
},
header: {
display: 'grid',
@@ -52,7 +60,9 @@ const tableStyles = {
borderBottom: '1px solid var(--jp-border-color2)',
fontWeight: 600,
fontSize: '12px',
- color: 'var(--jp-ui-font-color0)'
+ color: 'var(--jp-ui-font-color2)',
+ textTransform: 'uppercase' as const,
+ letterSpacing: '0.5px'
},
headerCell: {
padding: '8px 12px',
@@ -103,6 +113,7 @@ const tableStyles = {
height: '18px',
px: 0.75,
ml: 0.75,
+ my: -0.25,
borderRadius: 'var(--jp-border-radius)',
border: '1px solid var(--jp-border-color2)',
bgcolor: 'var(--jp-layout-color1)',
@@ -112,6 +123,224 @@ const tableStyles = {
}
};
+/** Tenant information panel */
+const TenantInfo: FC<{
+ detail: ITenantDetailResponse | undefined;
+ isLoading: boolean;
+ error: Error | null;
+}> = ({ detail, isLoading, error }) => {
+ if (isLoading) {
+ return (
+
+ Loading tenant information...
+
+ );
+ }
+ if (error) {
+ return (
+
+ Error loading tenant info: {error.message}
+
+ );
+ }
+ if (!detail) {
+ return null;
+ }
+
+ const { metadata, stewards, members } = detail;
+
+ const infoCardStyle = {
+ flex: 1,
+ minWidth: 0,
+ border: '1px solid var(--jp-border-color2)',
+ borderRadius: '4px',
+ overflow: 'hidden'
+ };
+
+ const infoCardHeader = {
+ padding: '8px 12px',
+ backgroundColor: 'var(--jp-layout-color2)',
+ borderBottom: '1px solid var(--jp-border-color2)',
+ fontWeight: 600,
+ fontSize: '12px',
+ color: 'var(--jp-ui-font-color2)',
+ textTransform: 'uppercase' as const,
+ letterSpacing: '0.5px'
+ };
+
+ const infoCardBody = {
+ padding: '8px 12px',
+ fontSize: '12px',
+ color: 'var(--jp-ui-font-color0)'
+ };
+
+ return (
+
+ {/* Tenant Info */}
+
+ Tenant Info
+
+
+ {metadata.display_name || metadata.tenant_name}
+
+ {metadata.display_name &&
+ metadata.display_name !== metadata.tenant_name && (
+
+ {metadata.tenant_name}
+
+ )}
+ {metadata.organization && (
+
+ {metadata.organization}
+
+ )}
+ {metadata.website && (
+
+
+ {metadata.website}
+
+
+ )}
+
+
+
+ {/* Description */}
+
+ Description
+
+ {metadata.description || (
+
+ No description
+
+ )}
+
+
+
+ {/* Data Steward(s) */}
+
+
+ Data Steward{stewards.length !== 1 ? 's' : ''}
+
+
+ {stewards.length === 0 && (
+
+ None assigned
+
+ )}
+ {stewards.map(steward => (
+
+ {steward.display_name}
+ {steward.email && (
+
+ {steward.email}
+
+ )}
+
+ ))}
+
+
+
+ {/* Members */}
+
+
+ Members
+
+ {detail.member_count}
+
+
+
+ {members.map(member => (
+
+
+ {member.display_name} ({member.username})
+
+ ))}
+
+
+
+ );
+};
+
/** Databases list component */
const DatabasesList: FC<{
databases: string[];
@@ -388,6 +617,12 @@ export const TenantTabContent: FC = ({
});
}, [onSelectionChange, selectedDatabase, selectedTable, target.tenant]);
+ const tenantDetailQuery = useQuery({
+ queryKey: ['tenantDetail', target.tenant],
+ queryFn: () => fetchTenantDetail(target.tenant!),
+ enabled: !!target.tenant
+ });
+
const databasesQuery = useQuery({
queryKey: ['databases', target.tenant],
queryFn: () => fetchDatabases(target.tenant)
@@ -467,6 +702,19 @@ export const TenantTabContent: FC = ({
gap: 2
}}
>
+ {/* Tenant Information */}
+ {target.tenant && (
+
+ )}
+
{/* Top section: Databases and Tables */}
{/* Databases */}
diff --git a/src/api.ts b/src/api.ts
index cbf6424..24dc8c1 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -17,6 +17,53 @@ export interface IGroupsResponse {
group_count: number;
}
+/** Tenant metadata */
+export interface ITenantMetadata {
+ tenant_name: string;
+ display_name: string;
+ description: string | null;
+ website: string | null;
+ organization: string | null;
+ created_by: string;
+ created_at: string;
+ updated_at: string;
+ updated_by: string;
+}
+
+/** Tenant steward */
+export interface ITenantSteward {
+ username: string;
+ display_name: string;
+ email: string | null;
+ assigned_by: string;
+ assigned_at: string;
+}
+
+/** Tenant member */
+export interface ITenantMember {
+ username: string;
+ display_name: string;
+ email: string | null;
+ access_level: 'read_write' | 'read_only';
+ is_steward: boolean;
+}
+
+/** Tenant storage paths */
+export interface ITenantStoragePaths {
+ general_warehouse: string | null;
+ sql_warehouse: string | null;
+ namespace_prefix: string | null;
+}
+
+/** Full tenant detail response */
+export interface ITenantDetailResponse {
+ metadata: ITenantMetadata;
+ stewards: ITenantSteward[];
+ members: ITenantMember[];
+ member_count: number;
+ storage_paths: ITenantStoragePaths;
+}
+
/** Error response from server */
interface IErrorResponse {
error: string;
@@ -96,3 +143,14 @@ export async function fetchSchema(
const params = new URLSearchParams({ database, table });
return serverGet('schema', params);
}
+
+/**
+ * Fetch tenant detail including metadata, stewards, and members.
+ * @param tenant - Tenant name
+ */
+export async function fetchTenantDetail(
+ tenant: string
+): Promise {
+ const params = new URLSearchParams({ tenant });
+ return serverGet('tenant-detail', params);
+}
diff --git a/tenant_data_browser/cdm_methods.py b/tenant_data_browser/cdm_methods.py
index 2efcbe2..46d913d 100644
--- a/tenant_data_browser/cdm_methods.py
+++ b/tenant_data_browser/cdm_methods.py
@@ -63,4 +63,32 @@ def get_namespace_prefix(tenant=None, return_json=False):
from .mock_definitions import get_table_schema, get_databases, get_tables, get_my_groups, get_namespace_prefix
- return get_table_schema, get_databases, get_tables, get_my_groups, get_namespace_prefix, True
\ No newline at end of file
+ return get_table_schema, get_databases, get_tables, get_my_groups, get_namespace_prefix, True
+
+
+def get_tenant_detail_method():
+ """
+ Returns a get_tenant_detail function.
+ Tries to use the berdl_notebook_utils wrapper, falls back to mocks.
+ """
+ try:
+ from berdl_notebook_utils.minio_governance.tenant_management import (
+ get_tenant_detail as _berdl_get_tenant_detail,
+ )
+ logger.info("Using berdl_notebook_utils for tenant details")
+
+ def get_tenant_detail(tenant_name, return_json=False):
+ import json
+ response = _berdl_get_tenant_detail(tenant_name)
+ result = response.to_dict()
+ if return_json:
+ return json.dumps(result)
+ return result
+
+ return get_tenant_detail
+ except Exception as e:
+ logger.warning("berdl_notebook_utils import failed for tenant details: %s", e)
+ logger.info("Using mock get_tenant_detail")
+
+ from .mock_definitions import get_tenant_detail
+ return get_tenant_detail
diff --git a/tenant_data_browser/handlers.py b/tenant_data_browser/handlers.py
index 191acaa..186d63a 100644
--- a/tenant_data_browser/handlers.py
+++ b/tenant_data_browser/handlers.py
@@ -14,7 +14,7 @@
from jupyter_server.utils import url_path_join
import tornado.web
-from .cdm_methods import get_cdm_methods
+from .cdm_methods import get_cdm_methods, get_tenant_detail_method
logger = logging.getLogger(__name__)
@@ -28,6 +28,8 @@
using_mocks,
) = get_cdm_methods()
+get_tenant_detail = get_tenant_detail_method()
+
class BaseHandler(APIHandler):
"""Base handler with common utilities."""
@@ -168,6 +170,32 @@ async def get(self) -> None:
self.write_error_json(str(e), status=500)
+class TenantDetailHandler(BaseHandler):
+ """Handler for fetching tenant detail information."""
+
+ @tornado.web.authenticated
+ async def get(self) -> None:
+ """
+ GET /api/tenant-data-browser/tenant-detail?tenant=
+
+ Returns tenant metadata, stewards, members, and storage paths.
+ """
+ try:
+ tenant = self.get_argument("tenant")
+ except tornado.web.MissingArgumentError:
+ self.write_error_json("tenant query parameter is required", status=400)
+ return
+
+ try:
+ result = await self.run_sync(
+ get_tenant_detail, tenant, return_json=False
+ )
+ self.write_json(result)
+ except Exception as e:
+ logger.exception("Error fetching tenant detail for %s", tenant)
+ self.write_error_json(str(e), status=500)
+
+
def setup_handlers(web_app: Any) -> None:
"""Register handlers with the Jupyter server."""
host_pattern = ".*$"
@@ -179,6 +207,7 @@ def setup_handlers(web_app: Any) -> None:
(url_path_join(base_path, "databases"), DatabasesHandler),
(url_path_join(base_path, "tables"), TablesHandler),
(url_path_join(base_path, "schema"), SchemaHandler),
+ (url_path_join(base_path, "tenant-detail"), TenantDetailHandler),
]
web_app.add_handlers(host_pattern, handlers)
diff --git a/tenant_data_browser/mock_data.py b/tenant_data_browser/mock_data.py
index b93e5cd..0d53472 100644
--- a/tenant_data_browser/mock_data.py
+++ b/tenant_data_browser/mock_data.py
@@ -153,3 +153,228 @@
"created_at",
"updated_at",
]
+
+# Mock tenant detail responses keyed by tenant name
+MOCK_TENANT_DETAILS = {
+ "kbase": {
+ "metadata": {
+ "tenant_name": "kbase",
+ "display_name": "KBase Core",
+ "description": "Core KBase data including CDM tables, vocabularies, and genomics reference data.",
+ "website": "https://www.kbase.us",
+ "organization": "KBase",
+ "created_by": "admin",
+ "created_at": "2025-06-15T10:00:00",
+ "updated_at": "2026-01-20T14:30:00",
+ "updated_by": "admin",
+ },
+ "stewards": [
+ {
+ "username": "admin",
+ "display_name": "KBase Admin",
+ "email": "admin@kbase.us",
+ "assigned_by": "admin",
+ "assigned_at": "2025-06-15T10:00:00",
+ },
+ ],
+ "members": [
+ {
+ "username": "admin",
+ "display_name": "KBase Admin",
+ "email": "admin@kbase.us",
+ "access_level": "read_write",
+ "is_steward": True,
+ },
+ {
+ "username": MOCK_USERNAME,
+ "display_name": "Mock User",
+ "email": "mock_user@kbase.us",
+ "access_level": "read_only",
+ "is_steward": False,
+ },
+ ],
+ "member_count": 2,
+ "storage_paths": {
+ "general_warehouse": "s3a://cdm-lake/tenant-general-warehouse/kbase/",
+ "sql_warehouse": "s3a://cdm-lake/tenant-sql-warehouse/kbase/",
+ "namespace_prefix": "kbase_",
+ },
+ },
+ "globalusers": {
+ "metadata": {
+ "tenant_name": "globalusers",
+ "display_name": "Global Users",
+ "description": "Shared datasets and reference genomes accessible to all KBase users.",
+ "website": None,
+ "organization": "KBase",
+ "created_by": "admin",
+ "created_at": "2025-07-01T08:00:00",
+ "updated_at": "2026-02-10T11:00:00",
+ "updated_by": "admin",
+ },
+ "stewards": [
+ {
+ "username": "admin",
+ "display_name": "KBase Admin",
+ "email": "admin@kbase.us",
+ "assigned_by": "admin",
+ "assigned_at": "2025-07-01T08:00:00",
+ },
+ ],
+ "members": [
+ {
+ "username": "admin",
+ "display_name": "KBase Admin",
+ "email": "admin@kbase.us",
+ "access_level": "read_write",
+ "is_steward": True,
+ },
+ {
+ "username": MOCK_USERNAME,
+ "display_name": "Mock User",
+ "email": "mock_user@kbase.us",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ {
+ "username": "researcher1",
+ "display_name": "Alice Researcher",
+ "email": "alice@lbl.gov",
+ "access_level": "read_only",
+ "is_steward": False,
+ },
+ {
+ "username": "bchen",
+ "display_name": "Bob Chen",
+ "email": "bchen@lbl.gov",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ {
+ "username": "cpark",
+ "display_name": "Clara Park",
+ "email": "cpark@lbl.gov",
+ "access_level": "read_only",
+ "is_steward": False,
+ },
+ {
+ "username": "dkim",
+ "display_name": "David Kim",
+ "email": "dkim@lbl.gov",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ {
+ "username": "ewang",
+ "display_name": "Emily Wang",
+ "email": "ewang@lbl.gov",
+ "access_level": "read_only",
+ "is_steward": False,
+ },
+ {
+ "username": "fjones",
+ "display_name": "Frank Jones",
+ "email": "fjones@lbl.gov",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ {
+ "username": "glee",
+ "display_name": "Grace Lee",
+ "email": "glee@lbl.gov",
+ "access_level": "read_only",
+ "is_steward": False,
+ },
+ {
+ "username": "hsingh",
+ "display_name": "Hassan Singh",
+ "email": "hsingh@lbl.gov",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ {"username": "user10", "display_name": "Irene Taylor", "email": None, "access_level": "read_only", "is_steward": False},
+ {"username": "user11", "display_name": "Jake Martinez", "email": None, "access_level": "read_write", "is_steward": False},
+ {"username": "user12", "display_name": "Karen Wu", "email": None, "access_level": "read_only", "is_steward": False},
+ {"username": "user13", "display_name": "Leo Brown", "email": None, "access_level": "read_write", "is_steward": False},
+ {"username": "user14", "display_name": "Maria Garcia", "email": None, "access_level": "read_only", "is_steward": False},
+ {"username": "user15", "display_name": "Nate Wilson", "email": None, "access_level": "read_write", "is_steward": False},
+ {"username": "user16", "display_name": "Olivia Davis", "email": None, "access_level": "read_only", "is_steward": False},
+ {"username": "user17", "display_name": "Pat Thompson", "email": None, "access_level": "read_write", "is_steward": False},
+ {"username": "user18", "display_name": "Quinn Anderson", "email": None, "access_level": "read_only", "is_steward": False},
+ {"username": "user19", "display_name": "Rosa Hernandez", "email": None, "access_level": "read_write", "is_steward": False},
+ ],
+ "member_count": 20,
+ "storage_paths": {
+ "general_warehouse": "s3a://cdm-lake/tenant-general-warehouse/globalusers/",
+ "sql_warehouse": "s3a://cdm-lake/tenant-sql-warehouse/globalusers/",
+ "namespace_prefix": "globalusers_",
+ },
+ },
+ "demo": {
+ "metadata": {
+ "tenant_name": "demo",
+ "display_name": "Demo Environment",
+ "description": "Demonstration datasets for clinical trials, imaging, and laboratory data.",
+ "website": None,
+ "organization": "KBase",
+ "created_by": "admin",
+ "created_at": "2025-09-01T12:00:00",
+ "updated_at": "2026-03-01T09:00:00",
+ "updated_by": MOCK_USERNAME,
+ },
+ "stewards": [
+ {
+ "username": MOCK_USERNAME,
+ "display_name": "Mock User",
+ "email": "mock_user@kbase.us",
+ "assigned_by": "admin",
+ "assigned_at": "2026-01-15T10:00:00",
+ },
+ ],
+ "members": [
+ {
+ "username": MOCK_USERNAME,
+ "display_name": "Mock User",
+ "email": "mock_user@kbase.us",
+ "access_level": "read_write",
+ "is_steward": True,
+ },
+ {
+ "username": "researcher1",
+ "display_name": "Alice Researcher",
+ "email": "alice@lbl.gov",
+ "access_level": "read_write",
+ "is_steward": False,
+ },
+ ],
+ "member_count": 2,
+ "storage_paths": {
+ "general_warehouse": "s3a://cdm-lake/tenant-general-warehouse/demo/",
+ "sql_warehouse": "s3a://cdm-lake/tenant-sql-warehouse/demo/",
+ "namespace_prefix": "demo_",
+ },
+ },
+}
+
+# Default tenant detail for unknown tenants
+MOCK_DEFAULT_TENANT_DETAIL = {
+ "metadata": {
+ "tenant_name": "unknown",
+ "display_name": "Unknown Tenant",
+ "description": None,
+ "website": None,
+ "organization": None,
+ "created_by": "admin",
+ "created_at": "2026-01-01T00:00:00",
+ "updated_at": "2026-01-01T00:00:00",
+ "updated_by": "admin",
+ },
+ "stewards": [],
+ "members": [],
+ "member_count": 0,
+ "storage_paths": {
+ "general_warehouse": None,
+ "sql_warehouse": None,
+ "namespace_prefix": None,
+ },
+}
diff --git a/tenant_data_browser/mock_definitions.py b/tenant_data_browser/mock_definitions.py
index 64d9677..0e23030 100644
--- a/tenant_data_browser/mock_definitions.py
+++ b/tenant_data_browser/mock_definitions.py
@@ -18,6 +18,8 @@
MOCK_DEFAULT_COLUMNS,
MOCK_NAMESPACE_PREFIXES,
MOCK_USERNAME,
+ MOCK_TENANT_DETAILS,
+ MOCK_DEFAULT_TENANT_DETAIL,
)
@@ -102,6 +104,32 @@ def get_tables(database, use_hms=True, return_json=True):
return tables
+def get_tenant_detail(tenant_name, return_json=False):
+ """
+ Mock function that returns tenant detail information.
+
+ Args:
+ tenant_name (str): Name of the tenant
+ return_json (bool): Whether to return JSON string instead of dict
+
+ Returns:
+ dict or str: Tenant detail response
+ """
+ time.sleep(MOCK_DELAY)
+ detail = MOCK_TENANT_DETAILS.get(tenant_name)
+ if detail is None:
+ detail = {**MOCK_DEFAULT_TENANT_DETAIL}
+ detail["metadata"] = {
+ **MOCK_DEFAULT_TENANT_DETAIL["metadata"],
+ "tenant_name": tenant_name,
+ "display_name": tenant_name.replace("_", " ").title(),
+ }
+
+ if return_json:
+ return json.dumps(detail)
+ return detail
+
+
def get_namespace_prefix(tenant=None, return_json=False):
"""
Mock function that returns namespace prefix for a tenant.