mirror of
https://github.com/bec-project/bec_atlas.git
synced 2025-07-14 07:01:48 +02:00
feat: towards a first version
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Literal
|
||||
from typing import TYPE_CHECKING, Literal, Type, TypeVar
|
||||
|
||||
import pymongo
|
||||
from bec_lib.logger import bec_logger
|
||||
@ -11,6 +13,11 @@ from bec_atlas.model.model import User, UserCredentials
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bson import ObjectId
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class MongoDBDatasource:
|
||||
def __init__(self, config: dict) -> None:
|
||||
@ -107,19 +114,19 @@ class MongoDBDatasource:
|
||||
return UserCredentials(**out)
|
||||
|
||||
def find_one(
|
||||
self, collection: str, query_filter: dict, dtype: BaseModel, user: User | None = None
|
||||
) -> BaseModel | None:
|
||||
self, collection: str, query_filter: dict, dtype: Type[T], user: User | None = None
|
||||
) -> T | None:
|
||||
"""
|
||||
Find one document in the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
query_filter (dict): The filter to apply
|
||||
dtype (BaseModel): The data type to return
|
||||
dtype (Type[T]): The data type to return
|
||||
user (User): The user making the request
|
||||
|
||||
Returns:
|
||||
BaseModel: The data type with the document data
|
||||
T: The data type with the document data
|
||||
"""
|
||||
if user is not None:
|
||||
query_filter = self.add_user_filter(user, query_filter)
|
||||
@ -129,15 +136,15 @@ class MongoDBDatasource:
|
||||
return dtype(**out)
|
||||
|
||||
def find(
|
||||
self, collection: str, query_filter: dict, dtype: BaseModel, user: User | None = None
|
||||
) -> list[BaseModel]:
|
||||
self, collection: str, query_filter: dict, dtype: Type[T], user: User | None = None
|
||||
) -> list[T]:
|
||||
"""
|
||||
Find all documents in the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
query_filter (dict): The filter to apply
|
||||
dtype (BaseModel): The data type to return
|
||||
dtype (Type[T]): The data type to return
|
||||
user (User): The user making the request
|
||||
|
||||
Returns:
|
||||
@ -148,20 +155,88 @@ class MongoDBDatasource:
|
||||
out = self.db[collection].find(query_filter)
|
||||
return [dtype(**x) for x in out]
|
||||
|
||||
def post(self, collection: str, data: dict, dtype: Type[T], user: User | None = None) -> T:
|
||||
"""
|
||||
Post a single document to the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
data (dict): The data to insert
|
||||
dtype (Type[T]): The data type to return
|
||||
user (User): The user making the request
|
||||
|
||||
Returns:
|
||||
T: The data type with the document data
|
||||
"""
|
||||
if user is not None:
|
||||
data = self.add_user_filter(user, data, operation="w")
|
||||
out = self.db[collection].insert_one(data)
|
||||
return dtype(**data)
|
||||
|
||||
def patch(
|
||||
self,
|
||||
collection: str,
|
||||
id: ObjectId,
|
||||
update: dict,
|
||||
dtype: Type[T],
|
||||
user: User | None = None,
|
||||
return_document: bool = True,
|
||||
) -> T | None:
|
||||
"""
|
||||
Patch a single document in the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
id (ObjectId): The document id
|
||||
update (dict): The update to apply
|
||||
dtype (Type[T]): The data type to return
|
||||
user (User): The user making the request
|
||||
return_document (bool): When True, return the updated document, otherwise return the original document
|
||||
|
||||
Returns:
|
||||
Type[T]: The data type with the document data
|
||||
"""
|
||||
search_filter = {"_id": id}
|
||||
if user is not None:
|
||||
search_filter = self.add_user_filter(user, search_filter, operation="w")
|
||||
out = self.db[collection].find_one_and_update(
|
||||
filter=search_filter, update={"$set": update}, return_document=return_document
|
||||
)
|
||||
if out is None:
|
||||
return None
|
||||
return dtype(**out)
|
||||
|
||||
def delete_one(self, collection: str, filter: dict, user: User | None = None) -> bool:
|
||||
"""
|
||||
Delete a single document in the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
filter (dict): The filter to apply
|
||||
user (User): The user making the request
|
||||
|
||||
Returns:
|
||||
bool: True if the document was deleted, otherwise False
|
||||
"""
|
||||
if user is not None:
|
||||
filter = self.add_user_filter(user, filter, operation="w")
|
||||
out = self.db[collection].delete_one(filter)
|
||||
return out.deleted_count > 0
|
||||
|
||||
def aggregate(
|
||||
self, collection: str, pipeline: list[dict], dtype: BaseModel, user: User | None = None
|
||||
) -> list[BaseModel]:
|
||||
self, collection: str, pipeline: list[dict], dtype: Type[T], user: User | None = None
|
||||
) -> list[T]:
|
||||
"""
|
||||
Aggregate documents in the collection.
|
||||
|
||||
Args:
|
||||
collection (str): The collection name
|
||||
pipeline (list[dict]): The aggregation pipeline
|
||||
dtype (BaseModel): The data type to return
|
||||
dtype (Type[T]): The data type to return
|
||||
user (User): The user making the request
|
||||
|
||||
Returns:
|
||||
list[BaseModel]: The data type with the document data
|
||||
list[T]: The data type with the document data
|
||||
"""
|
||||
if user is not None:
|
||||
# Add the user filter to the lookup pipeline
|
||||
|
@ -7,7 +7,7 @@ from redis.asyncio import Redis as AsyncRedis
|
||||
from redis.exceptions import AuthenticationError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_atlas.model.model import Deployments
|
||||
from bec_atlas.model.model import DeploymentCredential
|
||||
|
||||
|
||||
class RedisDatasource:
|
||||
@ -63,31 +63,35 @@ class RedisDatasource:
|
||||
"default", enabled=True, categories=["-@all"], commands=["+auth", "+acl|whoami"]
|
||||
)
|
||||
|
||||
def add_deployment_acl(self, deployment: Deployments):
|
||||
def add_deployment_acl(self, deployment_credential: DeploymentCredential):
|
||||
"""
|
||||
Add ACLs for the deployment.
|
||||
|
||||
Args:
|
||||
deployment (Deployments): The deployment object
|
||||
"""
|
||||
print(f"Adding ACLs for deployment <{deployment.name}>({deployment.id})")
|
||||
print(f"Adding ACLs for deployment {deployment_credential.id}")
|
||||
dep_id = deployment_credential.id
|
||||
dep_key = deployment_credential.credential
|
||||
self.connector._redis_conn.acl_setuser(
|
||||
f"ingestor_{deployment.id}",
|
||||
f"ingestor_{dep_id}",
|
||||
enabled=True,
|
||||
passwords=f"+{deployment.deployment_key}",
|
||||
passwords=f"+{dep_key}",
|
||||
categories=["+@all", "-@dangerous"],
|
||||
keys=[
|
||||
f"internal/deployment/{deployment.id}/*",
|
||||
f"internal/deployment/{deployment.id}/*/state",
|
||||
f"internal/deployment/{deployment.id}/*/data/*",
|
||||
f"internal/deployment/{dep_id}/*",
|
||||
f"internal/deployment/{dep_id}/*/state",
|
||||
f"internal/deployment/{dep_id}/*/data/*",
|
||||
f"internal/deployment/{dep_id}/bec_access",
|
||||
],
|
||||
channels=[
|
||||
f"internal/deployment/{deployment.id}/*/state",
|
||||
f"internal/deployment/{deployment.id}/*",
|
||||
f"internal/deployment/{deployment.id}/request",
|
||||
f"internal/deployment/{deployment.id}/request_response/*",
|
||||
f"internal/deployment/{dep_id}/*/state",
|
||||
f"internal/deployment/{dep_id}/*",
|
||||
f"internal/deployment/{dep_id}/request",
|
||||
f"internal/deployment/{dep_id}/request_response/*",
|
||||
f"internal/deployment/{dep_id}/bec_access",
|
||||
],
|
||||
commands=[f"+keys|internal/deployment/{deployment.id}/*/state"],
|
||||
commands=[f"+keys|internal/deployment/{dep_id}/*/state"],
|
||||
reset_channels=True,
|
||||
reset_keys=True,
|
||||
)
|
||||
|
@ -163,7 +163,7 @@ class DataIngestor:
|
||||
self.handle_message(out, deployment_id)
|
||||
self.redis._redis_conn.xack(stream, "ingestor", message[0])
|
||||
|
||||
def handle_message(self, msg_dict: dict, deploymend_id: str):
|
||||
def handle_message(self, msg_dict: dict, deployment_id: str):
|
||||
"""
|
||||
Handle a message from the Redis queue.
|
||||
|
||||
@ -177,7 +177,7 @@ class DataIngestor:
|
||||
return
|
||||
|
||||
if isinstance(data, messages.ScanStatusMessage):
|
||||
self.update_scan_status(data, deploymend_id)
|
||||
self.update_scan_status(data, deployment_id)
|
||||
|
||||
@lru_cache()
|
||||
def get_default_session_id(self, deployment_id: str):
|
||||
|
@ -1,7 +1,11 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from bec_atlas.datasources.datasource_manager import DatasourceManager
|
||||
from bec_atlas.router.bec_access_router import BECAccessRouter
|
||||
from bec_atlas.router.deployment_access_router import DeploymentAccessRouter
|
||||
from bec_atlas.router.deployment_credentials import DeploymentCredentialsRouter
|
||||
from bec_atlas.router.deployments_router import DeploymentsRouter
|
||||
from bec_atlas.router.realm_router import RealmRouter
|
||||
from bec_atlas.router.redis_router import RedisRouter, RedisWebsocket
|
||||
@ -13,6 +17,8 @@ CONFIG = {
|
||||
"mongodb": {"host": "localhost", "port": 27017},
|
||||
}
|
||||
|
||||
origins = ["http://localhost:4200", "http://localhost"]
|
||||
|
||||
|
||||
class AtlasApp:
|
||||
API_VERSION = "v1"
|
||||
@ -20,6 +26,13 @@ class AtlasApp:
|
||||
def __init__(self, config=None):
|
||||
self.config = config or CONFIG
|
||||
self.app = FastAPI()
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
self.server = None
|
||||
self.prefix = f"/api/{self.API_VERSION}"
|
||||
self.datasources = DatasourceManager(config=self.config)
|
||||
@ -43,12 +56,31 @@ class AtlasApp:
|
||||
raise ValueError("Datasources not loaded")
|
||||
self.scan_router = ScanRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.scan_router.router, tags=["Scan"])
|
||||
|
||||
self.user_router = UserRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.user_router.router, tags=["User"])
|
||||
|
||||
self.deployment_router = DeploymentsRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.deployment_router.router, tags=["Deployment"])
|
||||
|
||||
self.deployment_credentials_router = DeploymentCredentialsRouter(
|
||||
prefix=self.prefix, datasources=self.datasources
|
||||
)
|
||||
self.app.include_router(
|
||||
self.deployment_credentials_router.router, tags=["Deployment Credentials"]
|
||||
)
|
||||
|
||||
self.deployment_access_router = DeploymentAccessRouter(
|
||||
prefix=self.prefix, datasources=self.datasources
|
||||
)
|
||||
self.app.include_router(self.deployment_access_router.router, tags=["Deployment Access"])
|
||||
|
||||
self.bec_access_router = BECAccessRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.bec_access_router.router, tags=["BEC Access"])
|
||||
|
||||
self.realm_router = RealmRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.realm_router.router, tags=["Realm"])
|
||||
|
||||
self.redis_router = RedisRouter(prefix=self.prefix, datasources=self.datasources)
|
||||
self.app.include_router(self.redis_router.router, tags=["Redis"])
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Literal
|
||||
|
||||
from bec_lib import messages
|
||||
@ -53,7 +52,6 @@ class UserInfo(BaseModel):
|
||||
class Deployments(MongoBaseModel, AccessProfile):
|
||||
realm_id: str | ObjectId
|
||||
name: str
|
||||
deployment_key: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
active_session_id: str | ObjectId | None = None
|
||||
config_templates: list[str | ObjectId] = []
|
||||
|
||||
@ -61,11 +59,57 @@ class Deployments(MongoBaseModel, AccessProfile):
|
||||
class DeploymentsPartial(MongoBaseModel, AccessProfilePartial):
|
||||
realm_id: str | ObjectId | None = None
|
||||
name: str | None = None
|
||||
deployment_key: str | None = None
|
||||
active_session_id: str | ObjectId | None = None
|
||||
config_templates: list[str | ObjectId] | None = None
|
||||
|
||||
|
||||
class DeploymentCredential(MongoBaseModel):
|
||||
credential: str
|
||||
|
||||
|
||||
class DeploymentAccess(MongoBaseModel, AccessProfile):
|
||||
"""
|
||||
The DeploymentAccess model is used to store the access control
|
||||
lists for the deployment. The access control lists are used to
|
||||
control access to the BEC deployment and contain either user
|
||||
or group names.
|
||||
Once the access control lists are updated, the corresponding
|
||||
BECAccessProfiles for this deployment are updated to reflect
|
||||
the changes.
|
||||
|
||||
Owner: beamline staff
|
||||
"""
|
||||
|
||||
user_read_access: list[str] = []
|
||||
user_write_access: list[str] = []
|
||||
su_read_access: list[str] = []
|
||||
su_write_access: list[str] = []
|
||||
remote_access: list[str] = []
|
||||
|
||||
|
||||
class BECAccessProfile(MongoBaseModel, AccessProfile):
|
||||
"""
|
||||
The BECAccessProfile model is used to store the Redis ACL config
|
||||
for BEC of a user. The username can be either a user or a group.
|
||||
The config fields (categories, keys, channels, commands) are determined
|
||||
based on the access level given through the corresponding DeploymentAccess
|
||||
document.
|
||||
|
||||
Owner: admin
|
||||
Access: user or group matching the username
|
||||
|
||||
"""
|
||||
|
||||
deployment_id: str | ObjectId
|
||||
username: str
|
||||
passwords: dict[str, str] = {}
|
||||
categories: list[str] = []
|
||||
keys: list[str] = []
|
||||
channels: list[str] = []
|
||||
commands: list[str] = []
|
||||
profile: str = ""
|
||||
|
||||
|
||||
class Realm(MongoBaseModel, AccessProfile):
|
||||
realm_id: str
|
||||
deployments: list[Deployments | DeploymentsPartial] = []
|
||||
|
51
backend/bec_atlas/router/bec_access_router.py
Normal file
51
backend/bec_atlas/router/bec_access_router.py
Normal file
@ -0,0 +1,51 @@
|
||||
from bson import ObjectId
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from bec_atlas.authentication import get_current_user
|
||||
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||
from bec_atlas.model.model import BECAccessProfile, UserInfo
|
||||
from bec_atlas.router.base_router import BaseRouter
|
||||
|
||||
|
||||
class BECAccessRouter(BaseRouter):
|
||||
def __init__(self, prefix="/api/v1", datasources=None):
|
||||
super().__init__(prefix, datasources)
|
||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/bec_access",
|
||||
self.get_bec_access,
|
||||
methods=["GET"],
|
||||
description="Retrieve the access key for a specific deployment and user.",
|
||||
)
|
||||
|
||||
async def get_bec_access(
|
||||
self,
|
||||
deployment_id: str,
|
||||
user: str = Query(None),
|
||||
current_user: UserInfo = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
Retrieve the access key for a specific deployment and user.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
user (str): The user name to retrieve the access key for. If not provided,
|
||||
the access key for the current user will be retrieved.
|
||||
current_user (UserInfo): The current user
|
||||
"""
|
||||
if not user:
|
||||
user = current_user.email
|
||||
out = self.db.find_one(
|
||||
"bec_access_profiles",
|
||||
{"deployment_id": ObjectId(deployment_id), "username": user},
|
||||
BECAccessProfile,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
if not out:
|
||||
raise HTTPException(status_code=404, detail="Access key not found.")
|
||||
|
||||
# Return the newest access key
|
||||
timestamps = sorted(out.passwords.keys())
|
||||
return {"token": out.passwords[timestamps[-1]]}
|
227
backend/bec_atlas/router/deployment_access_router.py
Normal file
227
backend/bec_atlas/router/deployment_access_router.py
Normal file
@ -0,0 +1,227 @@
|
||||
import secrets
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from bec_lib.endpoints import EndpointInfo, MessageOp
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
from bson import ObjectId
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from bec_atlas.authentication import get_current_user
|
||||
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||
from bec_atlas.model.model import BECAccessProfile, DeploymentAccess, UserInfo
|
||||
from bec_atlas.router.base_router import BaseRouter
|
||||
from bec_atlas.router.redis_router import RedisAtlasEndpoints
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_atlas.datasources.redis_datasource import RedisDatasource
|
||||
|
||||
|
||||
class DeploymentAccessRouter(BaseRouter):
|
||||
def __init__(self, prefix="/api/v1", datasources=None):
|
||||
super().__init__(prefix, datasources)
|
||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/deployment_access",
|
||||
self.get_deployment_access,
|
||||
methods=["GET"],
|
||||
description="Get the access lists for a specific deployment.",
|
||||
response_model=DeploymentAccess,
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/deployment_access",
|
||||
self.patch_deployment_access,
|
||||
methods=["PATCH"],
|
||||
description="Update the access lists for a specific deployment.",
|
||||
response_model=DeploymentAccess,
|
||||
)
|
||||
|
||||
async def get_deployment_access(
|
||||
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||
) -> DeploymentAccess:
|
||||
"""
|
||||
Get the access lists for a specific deployment.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
current_user (UserInfo): The current user
|
||||
|
||||
Returns:
|
||||
DeploymentAccess: The access lists for the deployment
|
||||
"""
|
||||
return self.db.find_one(
|
||||
"deployments", {"_id": ObjectId(deployment_id)}, DeploymentAccess, user=current_user
|
||||
)
|
||||
|
||||
async def patch_deployment_access(
|
||||
self,
|
||||
deployment_id: str,
|
||||
deployment_access: dict,
|
||||
current_user: UserInfo = Depends(get_current_user),
|
||||
) -> DeploymentAccess:
|
||||
"""
|
||||
Update the access lists for a specific deployment.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
deployment_access (DeploymentAccess): The deployment access object
|
||||
current_user (UserInfo): The current user
|
||||
|
||||
Returns:
|
||||
DeploymentAccess: The updated access lists for the deployment
|
||||
"""
|
||||
deployment_access.pop("_id", None)
|
||||
deployment_access.pop("id", None)
|
||||
deployment_access.pop("owner_groups", None)
|
||||
deployment_access.pop("access_groups", None)
|
||||
original = self.db.find_one(
|
||||
"deployment_access",
|
||||
{"_id": ObjectId(deployment_id)},
|
||||
DeploymentAccess,
|
||||
user=current_user,
|
||||
)
|
||||
out = self.db.patch(
|
||||
collection="deployment_access",
|
||||
id=ObjectId(deployment_id),
|
||||
update=deployment_access,
|
||||
dtype=DeploymentAccess,
|
||||
user=current_user,
|
||||
)
|
||||
self._update_bec_access_profiles(original=original, updated=out)
|
||||
self._refresh_redis_bec_access(deployment_id)
|
||||
return out
|
||||
|
||||
def _update_bec_access_profiles(self, original: DeploymentAccess, updated: DeploymentAccess):
|
||||
"""
|
||||
Update the BEC access profiles in the database. This will not update the redis access.
|
||||
Call _refresh_redis_bec_access to update the redis access.
|
||||
|
||||
Args:
|
||||
deployment_access (DeploymentAccess): The deployment access object
|
||||
"""
|
||||
db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
|
||||
new_profiles = set(
|
||||
updated.user_read_access
|
||||
+ updated.user_write_access
|
||||
+ updated.su_read_access
|
||||
+ updated.su_write_access
|
||||
)
|
||||
old_profiles = set(
|
||||
original.user_read_access
|
||||
+ original.user_write_access
|
||||
+ original.su_read_access
|
||||
+ original.su_write_access
|
||||
)
|
||||
removed_profiles = old_profiles - new_profiles
|
||||
for profile in removed_profiles:
|
||||
db.delete_one("bec_access_profiles", {"username": profile, "deployment_id": updated.id})
|
||||
for profile in new_profiles:
|
||||
if profile in updated.su_write_access:
|
||||
access = self._get_redis_access_profile("su_write", profile, updated.id)
|
||||
elif profile in updated.su_read_access:
|
||||
access = self._get_redis_access_profile("su_read", profile, updated.id)
|
||||
elif profile in updated.user_write_access:
|
||||
access = self._get_redis_access_profile("user_write", profile, updated.id)
|
||||
else:
|
||||
access = self._get_redis_access_profile("user_read", profile, updated.id)
|
||||
|
||||
existing_profile = db.find_one(
|
||||
"bec_access_profiles",
|
||||
{"username": profile, "deployment_id": updated.id},
|
||||
BECAccessProfile,
|
||||
)
|
||||
if existing_profile:
|
||||
# access.passwords = existing_profile.passwords
|
||||
db.patch(
|
||||
"bec_access_profiles",
|
||||
existing_profile.id,
|
||||
access.model_dump(exclude_none=True, exclude_defaults=True),
|
||||
BECAccessProfile,
|
||||
)
|
||||
else:
|
||||
access.passwords = {str(time.time()): secrets.token_urlsafe(32)}
|
||||
db.post(
|
||||
"bec_access_profiles", access.model_dump(exclude_none=True), BECAccessProfile
|
||||
)
|
||||
|
||||
def _refresh_redis_bec_access(self, deployment_id: str):
|
||||
"""
|
||||
Refresh the redis BEC access.
|
||||
"""
|
||||
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||
db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
profiles = db.find(
|
||||
"bec_access_profiles", {"deployment_id": ObjectId(deployment_id)}, BECAccessProfile
|
||||
)
|
||||
profiles = [profile.model_dump(exclude_none=True) for profile in profiles]
|
||||
for profile in profiles:
|
||||
profile.pop("owner_groups", None)
|
||||
profile.pop("access_groups", None)
|
||||
profile.pop("deployment_id", None)
|
||||
profile.pop("_id", None)
|
||||
|
||||
endpoint_info = EndpointInfo(
|
||||
RedisAtlasEndpoints.redis_bec_acl_user(deployment_id), Any, MessageOp.SET_PUBLISH
|
||||
)
|
||||
|
||||
redis.connector.set_and_publish(endpoint_info, MsgpackSerialization.dumps(profiles))
|
||||
|
||||
def _get_redis_access_profile(self, access_profile: str, username: str, deployment_id: str):
|
||||
"""
|
||||
Get the redis access profile.
|
||||
|
||||
Args:
|
||||
access_profile (str): The access profile
|
||||
username (str): The username
|
||||
deployment_id (str): The deployment id
|
||||
|
||||
"""
|
||||
if access_profile == "su_write":
|
||||
return BECAccessProfile(
|
||||
owner_groups=["admin"],
|
||||
access_groups=[username],
|
||||
deployment_id=deployment_id,
|
||||
username=username,
|
||||
categories=["+@all"],
|
||||
keys=["*"],
|
||||
channels=["*"],
|
||||
commands=["+all"],
|
||||
profile="su_write",
|
||||
)
|
||||
if access_profile == "su_read":
|
||||
return BECAccessProfile(
|
||||
owner_groups=["admin"],
|
||||
access_groups=[username],
|
||||
deployment_id=deployment_id,
|
||||
username=username,
|
||||
categories=["+@all", "-@dangerous"],
|
||||
keys=["*"],
|
||||
channels=["*"],
|
||||
commands=["+read"],
|
||||
profile="su_read",
|
||||
)
|
||||
if access_profile == "user_write":
|
||||
return BECAccessProfile(
|
||||
owner_groups=["admin"],
|
||||
access_groups=[username],
|
||||
deployment_id=deployment_id,
|
||||
username=username,
|
||||
categories=["+@all", "-@dangerous"],
|
||||
keys=["*"],
|
||||
channels=["*"],
|
||||
commands=["+write"],
|
||||
profile="user_write",
|
||||
)
|
||||
return BECAccessProfile(
|
||||
owner_groups=["admin"],
|
||||
access_groups=[username],
|
||||
deployment_id=deployment_id,
|
||||
username=username,
|
||||
categories=["+@all", "-@dangerous"],
|
||||
keys=["*"],
|
||||
channels=["*"],
|
||||
commands=["+read"],
|
||||
profile="user_read",
|
||||
)
|
85
backend/bec_atlas/router/deployment_credentials.py
Normal file
85
backend/bec_atlas/router/deployment_credentials.py
Normal file
@ -0,0 +1,85 @@
|
||||
import secrets
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bson import ObjectId
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from bec_atlas.authentication import get_current_user
|
||||
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||
from bec_atlas.model.model import DeploymentCredential, UserInfo
|
||||
from bec_atlas.router.base_router import BaseRouter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_atlas.datasources.redis_datasource import RedisDatasource
|
||||
|
||||
|
||||
class DeploymentCredentialsRouter(BaseRouter):
|
||||
def __init__(self, prefix="/api/v1", datasources=None):
|
||||
super().__init__(prefix, datasources)
|
||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/deploymentCredentials",
|
||||
self.deployment_credential,
|
||||
methods=["GET"],
|
||||
description="Retrieve the deployment key for a specific deployment.",
|
||||
response_model=DeploymentCredential,
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/deploymentCredentials/refresh",
|
||||
self.refresh_deployment_credentials,
|
||||
methods=["POST"],
|
||||
description="Refresh the deployment key for a specific deployment.",
|
||||
response_model=DeploymentCredential,
|
||||
)
|
||||
|
||||
async def deployment_credential(
|
||||
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||
) -> DeploymentCredential:
|
||||
"""
|
||||
Get the credentials for a deployment.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
"""
|
||||
if set(current_user.groups) & set(["admin", "bec_group"]):
|
||||
out = self.db.find(
|
||||
"deployment_credentials", {"_id": ObjectId(deployment_id)}, DeploymentCredential
|
||||
)
|
||||
if len(out) > 0:
|
||||
return out[0]
|
||||
return None
|
||||
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User does not have permission to access this resource."
|
||||
)
|
||||
|
||||
async def refresh_deployment_credentials(
|
||||
self, deployment_id: str, current_user: UserInfo = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Refresh the deployment credentials.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
|
||||
"""
|
||||
if set(current_user.groups) & set(["admin", "bec_group"]):
|
||||
token = secrets.token_urlsafe(32)
|
||||
out = self.db.patch(
|
||||
"deployment_credentials",
|
||||
id=ObjectId(deployment_id),
|
||||
update={"credential": token},
|
||||
dtype=DeploymentCredential,
|
||||
)
|
||||
if out is None:
|
||||
raise HTTPException(status_code=404, detail="Deployment not found")
|
||||
|
||||
# update the redis deployment key
|
||||
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||
redis.add_deployment_acl(out)
|
||||
|
||||
return out
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User does not have permission to access this resource."
|
||||
)
|
@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from bec_atlas.authentication import get_current_user
|
||||
from bec_atlas.datasources.mongodb.mongodb import MongoDBDatasource
|
||||
from bec_atlas.model.model import Deployments, UserInfo
|
||||
from bec_atlas.model.model import DeploymentCredential, Deployments, UserInfo
|
||||
from bec_atlas.router.base_router import BaseRouter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -18,14 +18,14 @@ class DeploymentsRouter(BaseRouter):
|
||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/deployments/realm/{realm}",
|
||||
"/deployments/realm",
|
||||
self.deployments,
|
||||
methods=["GET"],
|
||||
description="Get all deployments for the realm",
|
||||
response_model=list[Deployments],
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/deployments/id/{deployment_id}",
|
||||
"/deployments/id",
|
||||
self.deployment_with_id,
|
||||
methods=["GET"],
|
||||
description="Get a single deployment by id for a realm",
|
||||
@ -65,10 +65,11 @@ class DeploymentsRouter(BaseRouter):
|
||||
Update the available deployments.
|
||||
"""
|
||||
self.available_deployments = self.db.find("deployments", {}, Deployments)
|
||||
credentials = self.db.find("deployment_credentials", {}, DeploymentCredential)
|
||||
|
||||
redis: RedisDatasource = self.datasources.datasources.get("redis")
|
||||
msg = json.dumps([msg.model_dump() for msg in self.available_deployments])
|
||||
redis.connector.set_and_publish("deployments", msg)
|
||||
if redis.reconfigured_acls:
|
||||
for deployment in self.available_deployments:
|
||||
for deployment in credentials:
|
||||
redis.add_deployment_acl(deployment)
|
||||
|
@ -20,7 +20,7 @@ class RealmRouter(BaseRouter):
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/realms/{realm_id}",
|
||||
"/realms/id",
|
||||
self.realm_with_id,
|
||||
methods=["GET"],
|
||||
description="Get a single realm by id",
|
||||
|
@ -95,6 +95,19 @@ class RedisAtlasEndpoints:
|
||||
"""
|
||||
return f"internal/deployment/{deployment}/request_response/{request_id}"
|
||||
|
||||
@staticmethod
|
||||
def redis_bec_acl_user(deployment_id: str):
|
||||
"""
|
||||
Endpoint for the redis BEC ACL user for a deployment.
|
||||
|
||||
Args:
|
||||
deployment_id (str): The deployment id
|
||||
|
||||
Returns:
|
||||
str: The endpoint for the redis BEC ACL user
|
||||
"""
|
||||
return f"internal/deployment/{deployment_id}/bec_access"
|
||||
|
||||
|
||||
class MsgResponse(Response):
|
||||
media_type = "application/json"
|
||||
@ -115,7 +128,7 @@ class RedisRouter(BaseRouter):
|
||||
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/redis/{deployment}", self.redis_get, methods=["GET"], response_class=MsgResponse
|
||||
"/redis", self.redis_get, methods=["GET"], response_class=MsgResponse
|
||||
)
|
||||
self.router.add_api_route("/redis", self.redis_post, methods=["POST"])
|
||||
self.router.add_api_route("/redis", self.redis_delete, methods=["DELETE"])
|
||||
|
@ -12,14 +12,14 @@ class ScanRouter(BaseRouter):
|
||||
self.db: MongoDBDatasource = self.datasources.datasources.get("mongodb")
|
||||
self.router = APIRouter(prefix=prefix)
|
||||
self.router.add_api_route(
|
||||
"/scans/session/{session_id}",
|
||||
"/scans/session",
|
||||
self.scans,
|
||||
methods=["GET"],
|
||||
description="Get all scans for a session",
|
||||
response_model=list[ScanStatus],
|
||||
)
|
||||
self.router.add_api_route(
|
||||
"/scans/id/{scan_id}",
|
||||
"/scans/id",
|
||||
self.scans_with_id,
|
||||
methods=["GET"],
|
||||
description="Get a single scan by id for a session",
|
||||
|
@ -39,13 +39,14 @@ class UserRouter(BaseRouter):
|
||||
return {"access_token": out, "token_type": "bearer"}
|
||||
|
||||
async def user_login(self, user_login: UserLoginRequest):
|
||||
exc = HTTPException(status_code=401, detail="User not found or password is incorrect")
|
||||
user = self.db.get_user_by_email(user_login.username)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise exc
|
||||
credentials = self.db.get_user_credentials(user.id)
|
||||
if credentials is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise exc
|
||||
if not verify_password(user_login.password, credentials.password):
|
||||
raise HTTPException(status_code=401, detail="Invalid password")
|
||||
raise exc
|
||||
|
||||
return create_access_token(data={"groups": list(user.groups), "email": user.email})
|
||||
|
@ -1,3 +1,5 @@
|
||||
import secrets
|
||||
|
||||
import pymongo
|
||||
|
||||
from bec_atlas.model import Deployments, Realm, Session
|
||||
@ -43,8 +45,29 @@ class DemoSetupLoader:
|
||||
owner_groups=["admin", "demo"],
|
||||
access_groups=["demo"],
|
||||
)
|
||||
if self.db["deployments"].find_one({"name": deployment.name}) is None:
|
||||
existing_deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||
if existing_deployment is None:
|
||||
self.db["deployments"].insert_one(deployment.__dict__)
|
||||
existing_deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||
deployment = existing_deployment
|
||||
|
||||
if self.db["deployment_credentials"].find_one({"_id": deployment["_id"]}) is None:
|
||||
deployment_credential = {
|
||||
"_id": deployment["_id"],
|
||||
"credential": secrets.token_urlsafe(32),
|
||||
}
|
||||
self.db["deployment_credentials"].insert_one(deployment_credential)
|
||||
deployment_access = {
|
||||
"_id": deployment["_id"],
|
||||
"owner_groups": ["admin", "demo"],
|
||||
"access_groups": [],
|
||||
"user_read_access": [],
|
||||
"user_write_access": [],
|
||||
"su_read_access": [],
|
||||
"su_write_access": [],
|
||||
"remote_access": [],
|
||||
}
|
||||
self.db["deployment_access"].insert_one(deployment_access)
|
||||
|
||||
if self.db["sessions"].find_one({"name": "_default_"}) is None:
|
||||
deployment = self.db["deployments"].find_one({"name": deployment.name})
|
||||
|
@ -20,9 +20,7 @@
|
||||
"outputPath": "dist/bec_atlas",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
@ -35,7 +33,7 @@
|
||||
"@angular/material/prebuilt-themes/cyan-orange.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": ["./node_modules/plotly.js/dist/plotly.min.js"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@ -79,10 +77,7 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"polyfills": ["zone.js", "zone.js/testing"],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
|
5109
frontend/bec_atlas/package-lock.json
generated
5109
frontend/bec_atlas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,6 +23,7 @@
|
||||
"angular-gridster2": "^19.0.0",
|
||||
"gridstack": "^11.1.2",
|
||||
"gridstack-angular": "^0.6.0-dev",
|
||||
"plotly.js": "^2.35.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tslib": "^2.3.0",
|
||||
@ -33,6 +34,7 @@
|
||||
"@angular/cli": "^19.0.5",
|
||||
"@angular/compiler-cli": "^19.0.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/plotly.js": "^2.35.2",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
@ -41,4 +43,4 @@
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
frontend/bec_atlas/src/app/app-config.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/app-config.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppConfigService } from './app-config.service';
|
||||
|
||||
describe('AppConfigService', () => {
|
||||
let service: AppConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AppConfigService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
27
frontend/bec_atlas/src/app/app-config.service.ts
Normal file
27
frontend/bec_atlas/src/app/app-config.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface AppConfig {
|
||||
baseUrl?: string;
|
||||
}
|
||||
@Injectable()
|
||||
export class AppConfigService {
|
||||
private appConfig: object | undefined = {};
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
async loadAppConfig(): Promise<void> {
|
||||
try {
|
||||
this.appConfig = await firstValueFrom(
|
||||
this.http.get('/assets/config.json')
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('No config provided, applying defaults');
|
||||
}
|
||||
}
|
||||
|
||||
getConfig(): AppConfig {
|
||||
return this.appConfig as AppConfig;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
<!-- <app-dashboard></app-dashboard> -->
|
||||
<!-- <app-gridstack-test></app-gridstack-test> -->
|
||||
<app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
||||
<!-- <app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
||||
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
|
||||
<app-queue-table></app-queue-table>
|
||||
<app-queue-table></app-queue-table> -->
|
||||
<router-outlet />
|
||||
|
@ -14,8 +14,6 @@ import { QueueTableComponent } from './queue-table/queue-table.component';
|
||||
DashboardComponent,
|
||||
CommonModule,
|
||||
GridStackTestComponent,
|
||||
DeviceBoxComponent,
|
||||
QueueTableComponent,
|
||||
],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
|
@ -1,17 +1,34 @@
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ApplicationConfig,
|
||||
provideEnvironmentInitializer,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { GridstackComponent } from 'gridstack/dist/angular';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { AppConfigService } from './app-config.service';
|
||||
import {
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
|
||||
const appConfigInitializerFn = (appConfig: AppConfigService) => {
|
||||
return () => appConfig.loadAppConfig();
|
||||
};
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes), provideAnimationsAsync(),
|
||||
provideRouter(routes),
|
||||
provideAnimationsAsync(),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
AppConfigService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: appConfigInitializerFn,
|
||||
deps: [AppConfigService],
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -1,3 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
|
||||
export const routes: Routes = [];
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: '**', redirectTo: 'login' }
|
||||
];
|
||||
|
16
frontend/bec_atlas/src/app/core/auth.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/core/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
39
frontend/bec_atlas/src/app/core/auth.service.ts
Normal file
39
frontend/bec_atlas/src/app/core/auth.service.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { shareReplay, timeout } from 'rxjs/operators';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { AuthDataService } from './remote-data.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
forceReload = false;
|
||||
constructor(private dataService: AuthDataService) {}
|
||||
|
||||
login(principal: string, password: string) {
|
||||
return this.dataService.login(principal, password).pipe(
|
||||
timeout(3000),
|
||||
tap((res) => {
|
||||
this.setSession(res);
|
||||
}),
|
||||
shareReplay()
|
||||
);
|
||||
}
|
||||
|
||||
setSession(authResult: string) {
|
||||
console.log(authResult);
|
||||
// it would be good to get an expiration date for the token...
|
||||
localStorage.setItem('id_token', authResult);
|
||||
localStorage.setItem('id_session', this.getRandomId());
|
||||
}
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('id_token');
|
||||
localStorage.removeItem('id_session');
|
||||
this.forceReload = true;
|
||||
}
|
||||
|
||||
getRandomId() {
|
||||
return Math.floor(Math.random() * 1000 + 1).toString();
|
||||
}
|
||||
}
|
3
frontend/bec_atlas/src/app/core/model/auth.ts
Normal file
3
frontend/bec_atlas/src/app/core/model/auth.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface AuthResponse {
|
||||
token: string;
|
||||
}
|
@ -27,13 +27,13 @@ export class RedisConnectorService {
|
||||
auth: {
|
||||
user: 'john_doe',
|
||||
token: '1234',
|
||||
deployment: '674739bc344eabfbabcff8bd',
|
||||
deployment: '678aa8d4875568640bd92176',
|
||||
},
|
||||
});
|
||||
|
||||
this.socket.onAny((event, ...args) => {
|
||||
console.log('Received event:', event, 'with data:', args);
|
||||
});
|
||||
// this.socket.onAny((event, ...args) => {
|
||||
// // console.log('Received event:', event, 'with data:', args);
|
||||
// });
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Connected to WebSocket server');
|
||||
@ -41,7 +41,7 @@ export class RedisConnectorService {
|
||||
});
|
||||
|
||||
this.socket.on('message', (data: any) => {
|
||||
console.log('Received message:', data);
|
||||
// console.log('Received message:', data);
|
||||
const dataObj = JSON.parse(data);
|
||||
const endpoint_signal = this.signals.get(dataObj.endpoint_request);
|
||||
if (endpoint_signal) {
|
||||
|
@ -40,4 +40,16 @@ export class MessageEndpoints {
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Endpoint for device monitor 2d
|
||||
*/
|
||||
static device_monitor_2d(device: string): EndpointInfo {
|
||||
const out: EndpointInfo = {
|
||||
endpoint: 'device_monitor_2d',
|
||||
args: [device],
|
||||
};
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
16
frontend/bec_atlas/src/app/core/remote-data.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/core/remote-data.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RemoteDataService } from './remote-data.service';
|
||||
|
||||
describe('RemoteDataService', () => {
|
||||
let service: RemoteDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(RemoteDataService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
75
frontend/bec_atlas/src/app/core/remote-data.service.ts
Normal file
75
frontend/bec_atlas/src/app/core/remote-data.service.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ServerSettingsService } from '../server-settings.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RemoteDataService {
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private serverSettings: ServerSettingsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Base method for making a POST request to the server
|
||||
* @param path path to the endpoint
|
||||
* @param payload payload to send
|
||||
* @param headers additional headers
|
||||
* @returns response from the server
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected post<T>(path: string, payload: any, headers: HttpHeaders) {
|
||||
return this.httpClient.post<T>(
|
||||
this.serverSettings.getServerAddress() + path,
|
||||
payload,
|
||||
{
|
||||
headers,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base method for making a GET request to the server
|
||||
* @param path path to the endpoint
|
||||
* @param params query parameters
|
||||
* @param headers additional headers
|
||||
* @returns response from the server
|
||||
*/
|
||||
protected get<T>(
|
||||
path: string,
|
||||
params: { [key: string]: string },
|
||||
headers: HttpHeaders
|
||||
) {
|
||||
return this.httpClient.get<T>(
|
||||
this.serverSettings.getServerAddress() + path,
|
||||
{
|
||||
headers,
|
||||
params,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthDataService extends RemoteDataService {
|
||||
/**
|
||||
* Method for logging into BEC
|
||||
* @param principal username or email
|
||||
* @param password password
|
||||
* @returns response from the server with the token
|
||||
* @throws HttpErrorResponse if the request fails
|
||||
* @throws TimeoutError if the request takes too long
|
||||
*/
|
||||
login(username: string, password: string) {
|
||||
let headers = new HttpHeaders();
|
||||
headers = headers.set('Content-Type', 'application/json; charset=utf-8');
|
||||
return this.post<string>(
|
||||
'user/login',
|
||||
{ username: username, password: password },
|
||||
headers
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +1,27 @@
|
||||
<div>
|
||||
<div class="gridster-container-toolbar">
|
||||
<gridster [options]="toolbarOptions">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboard; let i = index" class="gridster-item">
|
||||
{{ item }}
|
||||
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
|
||||
<div class="gridster-container">
|
||||
<gridster [options]="optionsEdit">
|
||||
<gridster-item [item]="item" *ngFor="let item of dashboard; let i = index" class="gridster-item">
|
||||
{{ item }}
|
||||
|
||||
</gridster-item>
|
||||
</gridster>
|
||||
</div>
|
||||
</div>
|
||||
<mat-sidenav-container class="sidenav-container">
|
||||
<mat-sidenav mode="side" opened>
|
||||
<button mat-button class="menu-item">
|
||||
<mat-icon>account_circle</mat-icon>
|
||||
</button>
|
||||
<button mat-button class="menu-item">
|
||||
<mat-icon>home</mat-icon>
|
||||
<span class="menu-text">Home</span>
|
||||
</button>
|
||||
<button mat-button class="menu-item">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span class="menu-text">Settings</span>
|
||||
</button>
|
||||
<button mat-button class="menu-item">
|
||||
<mat-icon>help</mat-icon>
|
||||
<span class="menu-text">Help</span>
|
||||
</button>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<div class="content">
|
||||
<app-device-box [device]="'samx'" [signal_name]="'samx'"></app-device-box>
|
||||
<app-device-box [device]="'samy'" [signal_name]="'samy'"></app-device-box>
|
||||
<app-queue-table></app-queue-table>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
@ -1,61 +1,49 @@
|
||||
.gridster-container {
|
||||
height: calc(100vh - 120px);
|
||||
background-color: rgb(37, 94, 75);
|
||||
}
|
||||
.gridster-container-toolbar {
|
||||
height: 50px;
|
||||
background-color: aqua;
|
||||
.sidenav-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gridster-container-toolbar .gridster-item {
|
||||
background-color: aqua;
|
||||
mat-sidenav {
|
||||
width: 200px;
|
||||
background: var(--mat-sys-primary);
|
||||
color: var(--mat-sys-on-primary);
|
||||
border-radius: 0px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::ng-deep .gridster {
|
||||
background-color: #848484;
|
||||
padding: 2px;
|
||||
.sidenav.collapsed {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
::ng-deep .gridster-edit{
|
||||
background-color:#ca000093;
|
||||
/* margin-top: 80px; */
|
||||
.menu-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
::ng-deep .gridster{
|
||||
background-color:#ca000093;
|
||||
/* margin-top: 80px; */
|
||||
}
|
||||
::ng-deep .gridster-edit{
|
||||
background-color:#ca000093;
|
||||
/* margin-top: 80px; */
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.gridster-item{
|
||||
padding: 10px 15px;
|
||||
background-color: #848484;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
.menu-item.hovered {
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
}
|
||||
|
||||
// .gridster-container{
|
||||
// /* top: 50px; */
|
||||
// padding-bottom: 100px;
|
||||
// padding-right: 20px;
|
||||
// padding-left: 20px;
|
||||
|
||||
// height: calc(100vh - 120px);
|
||||
// width: 100wh;
|
||||
// overflow: hidden;
|
||||
// }
|
||||
|
||||
.dashboard-item{
|
||||
height:inherit;
|
||||
overflow: inherit;
|
||||
|
||||
.menu-item mat-icon {
|
||||
color: var(--mat-sys-on-primary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.dashboard-edit{
|
||||
float: right;
|
||||
height: 36px;
|
||||
}
|
||||
.menu-item .menu-text {
|
||||
transition: opacity 0.3s ease;
|
||||
color: var(--mat-sys-on-primary);
|
||||
}
|
||||
|
||||
mat-sidenav-content {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
@ -1,145 +1,35 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { DeviceBoxComponent } from '../device-box/device-box.component';
|
||||
import { QueueTableComponent } from '../queue-table/queue-table.component';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CompactType,
|
||||
DisplayGrid,
|
||||
GridsterComponent,
|
||||
GridsterConfig,
|
||||
GridsterItem,
|
||||
GridsterItemComponent,
|
||||
GridType,
|
||||
} from 'angular-gridster2';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
imports: [
|
||||
DeviceBoxComponent,
|
||||
CommonModule,
|
||||
QueueTableComponent,
|
||||
MatSidenavModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
],
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.scss',
|
||||
imports: [CommonModule, GridsterItemComponent, GridsterComponent],
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
dashboard: Array<GridsterItem>;
|
||||
export class DashboardComponent {
|
||||
// isScreenSmall = false;
|
||||
|
||||
options: GridsterConfig = {
|
||||
gridType: GridType.Fit,
|
||||
compactType: CompactType.None,
|
||||
margin: 1,
|
||||
outerMargin: true,
|
||||
outerMarginTop: null,
|
||||
outerMarginRight: null,
|
||||
outerMarginBottom: null,
|
||||
outerMarginLeft: null,
|
||||
useTransformPositioning: true,
|
||||
mobileBreakpoint: 640,
|
||||
minCols: 40,
|
||||
maxCols: 40,
|
||||
minRows: 20,
|
||||
maxRows: 20,
|
||||
minColWidth: 300,
|
||||
maxItemCols: 100,
|
||||
minItemCols: 1,
|
||||
maxItemRows: 100,
|
||||
minItemRows: 1,
|
||||
maxItemArea: 2500,
|
||||
minItemArea: 1,
|
||||
defaultItemCols: 1,
|
||||
defaultItemRows: 1,
|
||||
// fixedColWidth: 105,
|
||||
// fixedRowHeight: 105,
|
||||
keepFixedHeightInMobile: false,
|
||||
keepFixedWidthInMobile: false,
|
||||
scrollSensitivity: 50,
|
||||
scrollSpeed: 20,
|
||||
enableEmptyCellClick: false,
|
||||
enableEmptyCellContextMenu: false,
|
||||
enableEmptyCellDrop: false,
|
||||
enableEmptyCellDrag: false,
|
||||
enableOccupiedCellDrop: false,
|
||||
emptyCellDragMaxCols: 50,
|
||||
emptyCellDragMaxRows: 50,
|
||||
ignoreMarginInRow: false,
|
||||
draggable: {
|
||||
enabled: true,
|
||||
},
|
||||
resizable: {
|
||||
enabled: true,
|
||||
},
|
||||
swap: true,
|
||||
pushItems: true,
|
||||
disablePushOnDrag: false,
|
||||
disablePushOnResize: false,
|
||||
pushDirections: { north: true, east: true, south: true, west: true },
|
||||
pushResizeItems: false,
|
||||
displayGrid: DisplayGrid.None,
|
||||
disableWindowResize: false,
|
||||
disableWarnings: false,
|
||||
scrollToNewItems: false,
|
||||
};
|
||||
|
||||
optionsEdit: GridsterConfig;
|
||||
toolbarOptions: GridsterConfig;
|
||||
|
||||
constructor() {
|
||||
this.dashboard = [];
|
||||
this.optionsEdit = JSON.parse(JSON.stringify(this.options));
|
||||
this.toolbarOptions = JSON.parse(JSON.stringify(this.options));
|
||||
}
|
||||
constructor(private breakpointObserver: BreakpointObserver) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.optionsEdit = JSON.parse(JSON.stringify(this.options)); // seriously??? I cannot believe that's the only way to perform a deep copy of an object
|
||||
this.optionsEdit.draggable = { enabled: true };
|
||||
this.optionsEdit.resizable = { enabled: true };
|
||||
this.optionsEdit.displayGrid = DisplayGrid.Always;
|
||||
this.toolbarOptions.minCols = 40;
|
||||
this.toolbarOptions.maxCols = 40;
|
||||
this.toolbarOptions.minRows = 1;
|
||||
this.toolbarOptions.maxRows = 1;
|
||||
|
||||
this.dashboard = [
|
||||
{ cols: 2, rows: 1, y: 0, x: 0 },
|
||||
{ cols: 2, rows: 2, y: 0, x: 2, hasContent: true },
|
||||
{ cols: 1, rows: 1, y: 0, x: 4 },
|
||||
{ cols: 1, rows: 1, y: 2, x: 5 },
|
||||
{ cols: 1, rows: 1, y: 1, x: 0 },
|
||||
{ cols: 1, rows: 1, y: 1, x: 0 },
|
||||
{
|
||||
cols: 2,
|
||||
rows: 2,
|
||||
y: 3,
|
||||
x: 5,
|
||||
minItemRows: 2,
|
||||
minItemCols: 2,
|
||||
label: 'Min rows & cols = 2',
|
||||
},
|
||||
{
|
||||
cols: 2,
|
||||
rows: 2,
|
||||
y: 2,
|
||||
x: 0,
|
||||
maxItemRows: 2,
|
||||
maxItemCols: 2,
|
||||
label: 'Max rows & cols = 2',
|
||||
},
|
||||
{
|
||||
cols: 2,
|
||||
rows: 1,
|
||||
y: 2,
|
||||
x: 2,
|
||||
dragEnabled: true,
|
||||
resizeEnabled: true,
|
||||
label: 'Drag&Resize Enabled',
|
||||
},
|
||||
{
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
y: 2,
|
||||
x: 4,
|
||||
dragEnabled: false,
|
||||
resizeEnabled: false,
|
||||
label: 'Drag&Resize Disabled',
|
||||
},
|
||||
{ cols: 1, rows: 1, y: 2, x: 6 },
|
||||
];
|
||||
|
||||
console.log('DashboardComponent initialized');
|
||||
// this.breakpointObserver
|
||||
// .observe([Breakpoints.Small, Breakpoints.XSmall])
|
||||
// .subscribe((result) => {
|
||||
// this.isScreenSmall = result.matches;
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
63
frontend/bec_atlas/src/app/login/login.component.html
Normal file
63
frontend/bec_atlas/src/app/login/login.component.html
Normal file
@ -0,0 +1,63 @@
|
||||
<div class="background-image"></div>
|
||||
<div class="login-form-flex">
|
||||
<mat-card class="mat-card">
|
||||
<mat-card-title>
|
||||
<!-- <img src="assets/scilog_logo.png" alt="BEC" width="120" height="120"> -->
|
||||
</mat-card-title>
|
||||
<mat-card-content class="content-container">
|
||||
<mat-tab-group
|
||||
mat-stretch-tabs="false"
|
||||
mat-align-tabs="center"
|
||||
fitInkBarToContent
|
||||
class="tab-container"
|
||||
dynamicHeight
|
||||
>
|
||||
<mat-tab label="Admin">
|
||||
<ng-template mat-tab-label>
|
||||
<label
|
||||
for="adminInput"
|
||||
matTooltip="Login with functional or admin accounts."
|
||||
matTooltipClass="example-tooltip-red1"
|
||||
[matTooltipPosition]="'above'"
|
||||
>
|
||||
Admin
|
||||
</label>
|
||||
</ng-template>
|
||||
<form [formGroup]="form" (keyup.enter)="login()" tabindex="0">
|
||||
<mat-form-field appearance="outline" class="mat-form-field">
|
||||
<mat-label>User name</mat-label>
|
||||
<input matInput [type]="'text'" formControlName="email" />
|
||||
</mat-form-field>
|
||||
<br />
|
||||
<mat-form-field appearance="outline" class="mat-form-field">
|
||||
<mat-label>Enter your password</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[type]="hide ? 'password' : 'text'"
|
||||
formControlName="password"
|
||||
/>
|
||||
<button
|
||||
mat-icon-button
|
||||
matSuffix
|
||||
(mousedown)="hide = !hide"
|
||||
[attr.aria-label]="'Hide password'"
|
||||
[attr.aria-pressed]="hide"
|
||||
tabindex="1"
|
||||
>
|
||||
<mat-icon>{{
|
||||
hide ? "visibility_off" : "visibility"
|
||||
}}</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<div class="errorText">
|
||||
{{ loginMessage }}
|
||||
</div>
|
||||
</form>
|
||||
<mat-card-actions class="mat-card-actions">
|
||||
<button mat-raised-button (click)="login()">Login</button>
|
||||
</mat-card-actions>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
36
frontend/bec_atlas/src/app/login/login.component.scss
Normal file
36
frontend/bec_atlas/src/app/login/login.component.scss
Normal file
@ -0,0 +1,36 @@
|
||||
// @import "../styles/colors";
|
||||
|
||||
.login-form-flex {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
background-image: url("../../assets/psi_background.jpg");
|
||||
filter: blur(4px);
|
||||
-webkit-filter: blur(4px);
|
||||
height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.mat-card-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
}
|
23
frontend/bec_atlas/src/app/login/login.component.spec.ts
Normal file
23
frontend/bec_atlas/src/app/login/login.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
87
frontend/bec_atlas/src/app/login/login.component.ts
Normal file
87
frontend/bec_atlas/src/app/login/login.component.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import {
|
||||
UntypedFormBuilder,
|
||||
UntypedFormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AuthService } from '../core/auth.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AppConfig, AppConfigService } from '../app-config.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatButtonModule,
|
||||
MatTabsModule,
|
||||
MatTooltipModule,
|
||||
MatIcon,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss',
|
||||
})
|
||||
export class LoginComponent {
|
||||
hide = true;
|
||||
form: UntypedFormGroup;
|
||||
loginMessage = ' ';
|
||||
appConfig!: AppConfig;
|
||||
|
||||
constructor(
|
||||
private appConfigService: AppConfigService,
|
||||
private fb: UntypedFormBuilder,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.appConfig = this.appConfigService.getConfig();
|
||||
this.form = this.fb.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.authService.forceReload) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async login() {
|
||||
const val = this.form.value;
|
||||
if (val.email && val.password) {
|
||||
try {
|
||||
const data = await firstValueFrom(
|
||||
this.authService.login(val.email, val.password)
|
||||
);
|
||||
console.log('User is logged in');
|
||||
this.router.navigateByUrl('/dashboard');
|
||||
} catch (error: unknown) {
|
||||
switch ((error as HttpErrorResponse).statusText) {
|
||||
case 'Unknown Error':
|
||||
this.loginMessage = 'Authentication failed.';
|
||||
return;
|
||||
case 'Unauthorized':
|
||||
this.loginMessage =
|
||||
'User name / email or password are not correct.';
|
||||
return;
|
||||
default:
|
||||
this.loginMessage = 'Authentication failed.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +1,34 @@
|
||||
<mat-card>
|
||||
<mat-toolbar color="primary">
|
||||
Queue Table
|
||||
</mat-toolbar>
|
||||
<mat-toolbar color="primary"> Queue Table </mat-toolbar>
|
||||
|
||||
<table mat-table [dataSource]="tableData()" class="mat-elevation-z8">
|
||||
<!-- Queue ID Column -->
|
||||
<ng-container matColumnDef="queue_id">
|
||||
<th mat-header-cell *matHeaderCellDef> Queue ID </th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.queue_id }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>Queue ID</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.queue_id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Scan ID Column -->
|
||||
<ng-container matColumnDef="scan_id">
|
||||
<th mat-header-cell *matHeaderCellDef> Scan ID </th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.scan_id }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>Scan ID</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.scan_id }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Scan Number Column -->
|
||||
<ng-container matColumnDef="scan_number">
|
||||
<th mat-header-cell *matHeaderCellDef> Scan Number </th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.scan_number }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>Scan Number</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.scan_number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Status Column -->
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef> Status </th>
|
||||
<td mat-cell *matCellDef="let element"> {{ element.status }} </td>
|
||||
<th mat-header-cell *matHeaderCellDef>Status</th>
|
||||
|
||||
<td mat-cell *matCellDef="let element">{{ element.status }}</td>
|
||||
</ng-container>
|
||||
<!-- Header Row -->
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<!-- Data Rows -->
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</mat-card>
|
||||
|
||||
|
@ -4,10 +4,11 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { RedisConnectorService } from '../core/redis-connector.service';
|
||||
import { MessageEndpoints } from '../core/redis_endpoints';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-queue-table',
|
||||
imports: [MatCardModule, MatTableModule, MatToolbarModule],
|
||||
imports: [MatCardModule, MatTableModule, MatToolbarModule, CommonModule],
|
||||
templateUrl: './queue-table.component.html',
|
||||
styleUrl: './queue-table.component.scss',
|
||||
})
|
||||
|
16
frontend/bec_atlas/src/app/server-settings.service.spec.ts
Normal file
16
frontend/bec_atlas/src/app/server-settings.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ServerSettingsService } from './server-settings.service';
|
||||
|
||||
describe('ServerSettingsService', () => {
|
||||
let service: ServerSettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ServerSettingsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
23
frontend/bec_atlas/src/app/server-settings.service.ts
Normal file
23
frontend/bec_atlas/src/app/server-settings.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppConfigService } from './app-config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ServerSettingsService {
|
||||
constructor(private appConfigService: AppConfigService) {}
|
||||
|
||||
getServerAddress() {
|
||||
return (
|
||||
this.appConfigService.getConfig().baseUrl ?? 'http://localhost/api/v1/'
|
||||
);
|
||||
}
|
||||
|
||||
getSocketAddress() {
|
||||
const baseUrl =
|
||||
this.appConfigService.getConfig().baseUrl ?? 'http://localhost/api/v1/';
|
||||
if (!baseUrl.startsWith('http'))
|
||||
throw new Error('BaseURL must use the http or https protocol');
|
||||
return `ws${baseUrl.substring(4)}`;
|
||||
}
|
||||
}
|
BIN
frontend/bec_atlas/src/assets/psi_background.jpg
Normal file
BIN
frontend/bec_atlas/src/assets/psi_background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 763 KiB |
Reference in New Issue
Block a user