Skip to content

Commit

Permalink
Improvements to ensemble selector (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenthoms authored Oct 16, 2023
1 parent d5cb21f commit 5bb5155
Show file tree
Hide file tree
Showing 20 changed files with 571 additions and 21 deletions.
2 changes: 2 additions & 0 deletions backend/src/backend/primary/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .routers.well.router import router as well_router
from .routers.seismic.router import router as seismic_router
from .routers.surface_polygons.router import router as surface_polygons_router
from .routers.graph.router import router as graph_router

logging.basicConfig(
level=logging.WARNING,
Expand Down Expand Up @@ -60,6 +61,7 @@ def custom_generate_unique_id(route: APIRoute) -> str:
app.include_router(well_router, prefix="/well", tags=["well"])
app.include_router(seismic_router, prefix="/seismic", tags=["seismic"])
app.include_router(surface_polygons_router, prefix="/surface_polygons", tags=["surface_polygons"])
app.include_router(graph_router, prefix="/graph", tags=["graph"])

authHelper = AuthHelper()
app.include_router(authHelper.router)
Expand Down
18 changes: 15 additions & 3 deletions backend/src/backend/primary/routers/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class FieldInfo(BaseModel):
class CaseInfo(BaseModel):
uuid: str
name: str
status: str
user: str


class EnsembleInfo(BaseModel):
Expand Down Expand Up @@ -64,11 +66,21 @@ async def get_cases(
if field_identifier == "DROGON":
for case_info in case_info_arr:
if case_info.uuid == "10f41041-2c17-4374-a735-bb0de62e29dc":
ret_arr.insert(0, CaseInfo(uuid=case_info.uuid, name=f"GOOD -- {case_info.name}"))
ret_arr.insert(
0,
CaseInfo(
uuid=case_info.uuid,
name=f"GOOD -- {case_info.name}",
status=case_info.status,
user=case_info.user,
),
)
else:
ret_arr.append(CaseInfo(uuid=case_info.uuid, name=case_info.name))
ret_arr.append(
CaseInfo(uuid=case_info.uuid, name=case_info.name, status=case_info.status, user=case_info.user)
)
else:
ret_arr = [CaseInfo(uuid=ci.uuid, name=ci.name) for ci in case_info_arr]
ret_arr = [CaseInfo(uuid=ci.uuid, name=ci.name, status=ci.status, user=ci.user) for ci in case_info_arr]

return ret_arr

Expand Down
4 changes: 2 additions & 2 deletions backend/src/backend/primary/routers/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ async def logged_in_user(
if authenticated_user.has_graph_access_token() and includeGraphApiInfo:
graph_api_access = GraphApiAccess(authenticated_user.get_graph_access_token())
try:
avatar_b64str_future = asyncio.create_task(graph_api_access.get_user_profile_photo())
graph_user_info_future = asyncio.create_task(graph_api_access.get_user_info())
avatar_b64str_future = asyncio.create_task(graph_api_access.get_user_profile_photo("me"))
graph_user_info_future = asyncio.create_task(graph_api_access.get_user_info("me"))

avatar_b64str = await avatar_b64str_future
graph_user_info = await graph_user_info_future
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions backend/src/backend/primary/routers/graph/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging

import httpx
from fastapi import APIRouter, Depends, Query

from src.backend.auth.auth_helper import AuthHelper
from src.services.utils.authenticated_user import AuthenticatedUser
from src.services.graph_access.graph_access import GraphApiAccess

from .schemas import GraphUserPhoto

LOGGER = logging.getLogger(__name__)

router = APIRouter()


@router.get("/user_photo/")
async def user_info(
# fmt:off
authenticated_user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user),
user_id: str = Query(description="User id"),
# fmt:on
) -> GraphUserPhoto:
"""Get username, display name and avatar from Microsoft Graph API for a given user id"""

user_photo = GraphUserPhoto(
avatar_b64str=None,
)

if authenticated_user.has_graph_access_token():
graph_api_access = GraphApiAccess(authenticated_user.get_graph_access_token())
try:
avatar_b64str = await graph_api_access.get_user_profile_photo(user_id)

user_photo.avatar_b64str = avatar_b64str
except httpx.HTTPError as exc:
print("Error while fetching user avatar and info from Microsoft Graph API (HTTP error):\n", exc)
except httpx.InvalidURL as exc:
print("Error while fetching user avatar and info from Microsoft Graph API (Invalid URL):\n", exc)

# Return 404 if no user info was found?
return user_photo
5 changes: 5 additions & 0 deletions backend/src/backend/primary/routers/graph/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class GraphUserPhoto(BaseModel):
avatar_b64str: str | None = None
2 changes: 1 addition & 1 deletion backend/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
SMDA_SUBSCRIPTION_KEY = os.environ["WEBVIZ_SMDA_SUBSCRIPTION_KEY"]
SMDA_RESOURCE_SCOPE = os.environ["WEBVIZ_SMDA_RESOURCE_SCOPE"]
SUMO_ENV = os.getenv("WEBVIZ_SUMO_ENV", "dev")
GRAPH_SCOPES = ["User.Read"]
GRAPH_SCOPES = ["User.Read", "User.ReadBasic.All"]

RESOURCE_SCOPES_DICT = {
"sumo": [f"api://{sumo_app_reg[SUMO_ENV]['RESOURCE_ID']}/access_as_user"],
Expand Down
16 changes: 10 additions & 6 deletions backend/src/services/graph_access/graph_access.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import base64
from typing import Mapping
from urllib.parse import urljoin

# Using the same http client as sumo
import httpx
Expand All @@ -8,6 +9,7 @@
class GraphApiAccess:
def __init__(self, access_token: str):
self._access_token = access_token
self.base_url = "https://graph.microsoft.com/v1.0/"

def _make_headers(self) -> Mapping[str, str]:
return {"Authorization": f"Bearer {self._access_token}"}
Expand All @@ -20,18 +22,20 @@ async def _request(self, url: str) -> httpx.Response:
)
return response

async def get_user_profile_photo(self) -> str | None:
print("entering get_user_profile_photo")
response = await self._request("https://graph.microsoft.com/v1.0/me/photo/$value")
async def get_user_profile_photo(self, user_id: str) -> str | None:
request_url = urljoin(self.base_url, "me/photo/$value" if user_id == "me" else f"users/{user_id}/photo/$value")

response = await self._request(request_url)

if response.status_code == 200:
return base64.b64encode(response.content).decode("utf-8")
else:
return None

async def get_user_info(self) -> Mapping[str, str] | None:
print("entering get_user_info")
response = await self._request("https://graph.microsoft.com/v1.0/me")
async def get_user_info(self, user_id: str) -> Mapping[str, str] | None:
request_url = urljoin(self.base_url, "me" if user_id == "me" else f"users/{user_id}")

response = await self._request(request_url)

if response.status_code == 200:
return response.json()
Expand Down
4 changes: 3 additions & 1 deletion backend/src/services/sumo_access/sumo_explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AssetInfo(BaseModel):
class CaseInfo(BaseModel):
uuid: str
name: str
status: str
user: str


class IterationInfo(BaseModel):
Expand Down Expand Up @@ -49,7 +51,7 @@ async def get_cases(self, field_identifier: str) -> List[CaseInfo]:

case_info_arr: List[CaseInfo] = []
async for case in case_collection:
case_info_arr.append(CaseInfo(uuid=case.uuid, name=case.name))
case_info_arr.append(CaseInfo(uuid=case.uuid, name=case.name, status=case.status, user=case.user))

# Sort on case name before returning
case_info_arr.sort(key=lambda case_info: case_info.name)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AxiosHttpRequest } from './core/AxiosHttpRequest';

import { DefaultService } from './services/DefaultService';
import { ExploreService } from './services/ExploreService';
import { GraphService } from './services/GraphService';
import { GridService } from './services/GridService';
import { InplaceVolumetricsService } from './services/InplaceVolumetricsService';
import { ParametersService } from './services/ParametersService';
Expand All @@ -24,6 +25,7 @@ export class ApiService {

public readonly default: DefaultService;
public readonly explore: ExploreService;
public readonly graph: GraphService;
public readonly grid: GridService;
public readonly inplaceVolumetrics: InplaceVolumetricsService;
public readonly parameters: ParametersService;
Expand Down Expand Up @@ -52,6 +54,7 @@ export class ApiService {

this.default = new DefaultService(this.request);
this.explore = new ExploreService(this.request);
this.graph = new GraphService(this.request);
this.grid = new GridService(this.request);
this.inplaceVolumetrics = new InplaceVolumetricsService(this.request);
this.parameters = new ParametersService(this.request);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type { EnsembleSensitivity as EnsembleSensitivity_api } from './models/En
export type { EnsembleSensitivityCase as EnsembleSensitivityCase_api } from './models/EnsembleSensitivityCase';
export type { FieldInfo as FieldInfo_api } from './models/FieldInfo';
export { Frequency as Frequency_api } from './models/Frequency';
export type { GraphUserPhoto as GraphUserPhoto_api } from './models/GraphUserPhoto';
export type { GridIntersection as GridIntersection_api } from './models/GridIntersection';
export type { GridSurface as GridSurface_api } from './models/GridSurface';
export type { HTTPValidationError as HTTPValidationError_api } from './models/HTTPValidationError';
Expand Down Expand Up @@ -57,6 +58,7 @@ export type { WellCompletionsZone as WellCompletionsZone_api } from './models/We

export { DefaultService } from './services/DefaultService';
export { ExploreService } from './services/ExploreService';
export { GraphService } from './services/GraphService';
export { GridService } from './services/GridService';
export { InplaceVolumetricsService } from './services/InplaceVolumetricsService';
export { ParametersService } from './services/ParametersService';
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/models/CaseInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
export type CaseInfo = {
uuid: string;
name: string;
status: string;
user: string;
};

8 changes: 8 additions & 0 deletions frontend/src/api/models/GraphUserPhoto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */

export type GraphUserPhoto = {
avatar_b64str: (string | null);
};

35 changes: 35 additions & 0 deletions frontend/src/api/services/GraphService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { GraphUserPhoto } from '../models/GraphUserPhoto';

import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';

export class GraphService {

constructor(public readonly httpRequest: BaseHttpRequest) {}

/**
* User Info
* Get username, display name and avatar from Microsoft Graph API for a given user id
* @param userId User id
* @returns GraphUserPhoto Successful Response
* @throws ApiError
*/
public userInfo(
userId: string,
): CancelablePromise<GraphUserPhoto> {
return this.httpRequest.request({
method: 'GET',
url: '/graph/user_photo/',
query: {
'user_id': userId,
},
errors: {
422: `Validation Error`,
},
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";

import { GraphUserPhoto_api } from "@api";
import { apiService } from "@framework/ApiService";
import { CircularProgress } from "@lib/components/CircularProgress";
import { AccountCircle } from "@mui/icons-material";
import { UseQueryResult, useQuery } from "@tanstack/react-query";

export type UserAvatarProps = {
userId: string;
};

const STALE_TIME = 60 * 1000;
const CACHE_TIME = 60 * 1000;

function useUserInfoQuery(userId: string): UseQueryResult<GraphUserPhoto_api> {
return useQuery({
queryKey: ["getUserInfo", userId],
queryFn: () => apiService.graph.userInfo(`${userId.toUpperCase()}@equinor.com`),
staleTime: STALE_TIME,
cacheTime: CACHE_TIME,
enabled: userId !== "",
});
}

export const UserAvatar: React.FC<UserAvatarProps> = (props) => {
const userInfo = useUserInfoQuery(props.userId);

if (userInfo.isFetching) {
return <CircularProgress size="medium-small" className="mr-1" />;
}

if (userInfo.data?.avatar_b64str) {
return (
<img
src={`data:image/png;base64,${userInfo.data.avatar_b64str}`}
alt="Avatar"
className="w-5 h-5 rounded-full mr-1"
title={props.userId}
/>
);
}
return (
<span title={props.userId}>
<AccountCircle className="w-5 h-5 mr-1" />
</span>
);
};
Loading

0 comments on commit 5bb5155

Please sign in to comment.