Updated history and frontend
This commit is contained in:
parent
347f8486af
commit
2fae856412
@ -8,32 +8,16 @@ from starlette.routing import Route as StarletteRoute
|
|||||||
|
|
||||||
from api.errors import unauthorized
|
from api.errors import unauthorized
|
||||||
from api.middleware.request import get_request
|
from api.middleware.request import get_request
|
||||||
|
from api.route_api_key_extension import RouteApiKeyExtension
|
||||||
from api.route_user_extension import RouteUserExtension
|
from api.route_user_extension import RouteUserExtension
|
||||||
from core.environment import Environment
|
from core.environment import Environment
|
||||||
from data.schemas.administration.api_key import ApiKey
|
from data.schemas.administration.api_key import ApiKey
|
||||||
from data.schemas.administration.api_key_dao import apiKeyDao
|
|
||||||
from data.schemas.administration.user import User
|
from data.schemas.administration.user import User
|
||||||
|
|
||||||
|
|
||||||
class Route(RouteUserExtension):
|
class Route(RouteUserExtension, RouteApiKeyExtension):
|
||||||
registered_routes: list[StarletteRoute] = []
|
registered_routes: list[StarletteRoute] = []
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def get_api_key(cls, request: Request) -> ApiKey:
|
|
||||||
auth_header = request.headers.get("Authorization", None)
|
|
||||||
api_key = auth_header.split(" ")[1]
|
|
||||||
return await apiKeyDao.find_by_key(api_key)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _verify_api_key(cls, req: Request) -> bool:
|
|
||||||
auth_header = req.headers.get("Authorization", None)
|
|
||||||
if not auth_header or not auth_header.startswith("API-Key "):
|
|
||||||
return False
|
|
||||||
|
|
||||||
api_key = auth_header.split(" ")[1]
|
|
||||||
api_key_from_db = await apiKeyDao.find_by_key(api_key)
|
|
||||||
return api_key_from_db is not None and not api_key_from_db.deleted
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _get_auth_type(
|
async def _get_auth_type(
|
||||||
cls, request: Request, auth_header: str
|
cls, request: Request, auth_header: str
|
||||||
@ -79,8 +63,7 @@ class Route(RouteUserExtension):
|
|||||||
return await cls._get_auth_type(request, auth_header)
|
return await cls._get_auth_type(request, auth_header)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def is_authorized(cls) -> bool:
|
async def is_authorized(cls, request: Request) -> bool:
|
||||||
request = get_request()
|
|
||||||
if request is None:
|
if request is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -119,7 +102,7 @@ class Route(RouteUserExtension):
|
|||||||
return await f(request, *args, **kwargs)
|
return await f(request, *args, **kwargs)
|
||||||
return f(request, *args, **kwargs)
|
return f(request, *args, **kwargs)
|
||||||
|
|
||||||
if not await cls.is_authorized():
|
if not await cls.is_authorized(request):
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
|
|
||||||
if iscoroutinefunction(f):
|
if iscoroutinefunction(f):
|
||||||
|
27
api/src/api/route_api_key_extension.py
Normal file
27
api/src/api/route_api_key_extension.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from data.schemas.administration.api_key import ApiKey
|
||||||
|
from data.schemas.administration.api_key_dao import apiKeyDao
|
||||||
|
|
||||||
|
|
||||||
|
class RouteApiKeyExtension:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_api_key(cls, request: Request) -> ApiKey:
|
||||||
|
auth_header = request.headers.get("Authorization", None)
|
||||||
|
api_key = auth_header.split(" ")[1]
|
||||||
|
return await apiKeyDao.find_single_by(
|
||||||
|
[{ApiKey.key: api_key}, {ApiKey.deleted: False}]
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _verify_api_key(cls, req: Request) -> bool:
|
||||||
|
auth_header = req.headers.get("Authorization", None)
|
||||||
|
if not auth_header or not auth_header.startswith("API-Key "):
|
||||||
|
return False
|
||||||
|
|
||||||
|
api_key = auth_header.split(" ")[1]
|
||||||
|
api_key_from_db = await apiKeyDao.find_single_by(
|
||||||
|
[{ApiKey.key: api_key}, {ApiKey.deleted: False}]
|
||||||
|
)
|
||||||
|
return api_key_from_db is not None and not api_key_from_db.deleted
|
@ -18,6 +18,7 @@ logger = Logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RouteUserExtension:
|
class RouteUserExtension:
|
||||||
|
_cached_users: dict[int, User] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_user_id_from_token(cls, request: Request) -> Optional[str]:
|
def _get_user_id_from_token(cls, request: Request) -> Optional[str]:
|
||||||
@ -62,9 +63,7 @@ class RouteUserExtension:
|
|||||||
if request is None:
|
if request is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await userDao.find_single_by(
|
return await userDao.find_by_keycloak_id(cls.get_token(request))
|
||||||
[{User.keycloak_id: cls.get_token(request)}, {User.deleted: False}]
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_user_or_default(cls) -> Optional[User]:
|
async def get_user_or_default(cls) -> Optional[User]:
|
||||||
|
@ -7,8 +7,7 @@ from core.database.abc.data_access_object_abc import DataAccessObjectABC
|
|||||||
|
|
||||||
class DbHistoryModelQueryABC(QueryABC):
|
class DbHistoryModelQueryABC(QueryABC):
|
||||||
|
|
||||||
def __init__(self, name: str = None):
|
def __init__(self, name: str = __name__):
|
||||||
assert name is not None, f"Name for {__name__} must be provided"
|
|
||||||
QueryABC.__init__(self, f"{name}History")
|
QueryABC.__init__(self, f"{name}History")
|
||||||
|
|
||||||
self.set_field("id", lambda x, *_: x.id)
|
self.set_field("id", lambda x, *_: x.id)
|
||||||
|
@ -4,6 +4,18 @@ type DomainResult {
|
|||||||
nodes: [Domain]
|
nodes: [Domain]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DomainHistory implements DbHistoryModel {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
|
||||||
|
shortUrls: [ShortUrl]
|
||||||
|
|
||||||
|
deleted: Boolean
|
||||||
|
editor: String
|
||||||
|
created: String
|
||||||
|
updated: String
|
||||||
|
}
|
||||||
|
|
||||||
type Domain implements DbModel {
|
type Domain implements DbModel {
|
||||||
id: Int
|
id: Int
|
||||||
name: String
|
name: String
|
||||||
@ -14,6 +26,8 @@ type Domain implements DbModel {
|
|||||||
editor: User
|
editor: User
|
||||||
created: String
|
created: String
|
||||||
updated: String
|
updated: String
|
||||||
|
|
||||||
|
history: [DomainHistory]
|
||||||
}
|
}
|
||||||
|
|
||||||
input DomainSort {
|
input DomainSort {
|
||||||
|
@ -4,6 +4,19 @@ type GroupResult {
|
|||||||
nodes: [Group]
|
nodes: [Group]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GroupHistory implements DbHistoryModel {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
|
||||||
|
shortUrls: [ShortUrl]
|
||||||
|
roles: [Role]
|
||||||
|
|
||||||
|
deleted: Boolean
|
||||||
|
editor: String
|
||||||
|
created: String
|
||||||
|
updated: String
|
||||||
|
}
|
||||||
|
|
||||||
type Group implements DbModel {
|
type Group implements DbModel {
|
||||||
id: Int
|
id: Int
|
||||||
name: String
|
name: String
|
||||||
@ -15,6 +28,7 @@ type Group implements DbModel {
|
|||||||
editor: User
|
editor: User
|
||||||
created: String
|
created: String
|
||||||
updated: String
|
updated: String
|
||||||
|
history: [GroupHistory]
|
||||||
}
|
}
|
||||||
|
|
||||||
input GroupSort {
|
input GroupSort {
|
||||||
|
@ -4,6 +4,17 @@ type PermissionResult {
|
|||||||
nodes: [Permission]
|
nodes: [Permission]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PermissionHistory implements DbHistoryModel {
|
||||||
|
id: Int
|
||||||
|
name: String
|
||||||
|
description: String
|
||||||
|
|
||||||
|
deleted: Boolean
|
||||||
|
editor: String
|
||||||
|
created: String
|
||||||
|
updated: String
|
||||||
|
}
|
||||||
|
|
||||||
type Permission implements DbModel {
|
type Permission implements DbModel {
|
||||||
id: Int
|
id: Int
|
||||||
name: String
|
name: String
|
||||||
@ -13,6 +24,8 @@ type Permission implements DbModel {
|
|||||||
editor: User
|
editor: User
|
||||||
created: String
|
created: String
|
||||||
updated: String
|
updated: String
|
||||||
|
|
||||||
|
history: [PermissionHistory]
|
||||||
}
|
}
|
||||||
|
|
||||||
input PermissionSort {
|
input PermissionSort {
|
||||||
@ -21,7 +34,7 @@ input PermissionSort {
|
|||||||
description: SortOrder
|
description: SortOrder
|
||||||
|
|
||||||
deleted: SortOrder
|
deleted: SortOrder
|
||||||
editorId: SortOrder
|
editor: UserSort
|
||||||
created: SortOrder
|
created: SortOrder
|
||||||
updated: SortOrder
|
updated: SortOrder
|
||||||
}
|
}
|
||||||
@ -32,7 +45,7 @@ input PermissionFilter {
|
|||||||
description: StringFilter
|
description: StringFilter
|
||||||
|
|
||||||
deleted: BooleanFilter
|
deleted: BooleanFilter
|
||||||
editor: IntFilter
|
editor: UserFilter
|
||||||
created: DateFilter
|
created: DateFilter
|
||||||
updated: DateFilter
|
updated: DateFilter
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,23 @@ type ShortUrlResult {
|
|||||||
nodes: [ShortUrl]
|
nodes: [ShortUrl]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShortUrlHistory implements DbHistoryModel {
|
||||||
|
id: Int
|
||||||
|
shortUrl: String
|
||||||
|
targetUrl: String
|
||||||
|
description: String
|
||||||
|
visits: Int
|
||||||
|
loadingScreen: Boolean
|
||||||
|
|
||||||
|
group: Group
|
||||||
|
domain: Domain
|
||||||
|
|
||||||
|
deleted: Boolean
|
||||||
|
editor: String
|
||||||
|
created: String
|
||||||
|
updated: String
|
||||||
|
}
|
||||||
|
|
||||||
type ShortUrl implements DbModel {
|
type ShortUrl implements DbModel {
|
||||||
id: Int
|
id: Int
|
||||||
shortUrl: String
|
shortUrl: String
|
||||||
@ -18,6 +35,7 @@ type ShortUrl implements DbModel {
|
|||||||
editor: User
|
editor: User
|
||||||
created: String
|
created: String
|
||||||
updated: String
|
updated: String
|
||||||
|
history: [ShortUrlHistory]
|
||||||
}
|
}
|
||||||
|
|
||||||
input ShortUrlSort {
|
input ShortUrlSort {
|
||||||
|
@ -9,6 +9,7 @@ type Subscription {
|
|||||||
settingChange: SubscriptionChange
|
settingChange: SubscriptionChange
|
||||||
userChange: SubscriptionChange
|
userChange: SubscriptionChange
|
||||||
userSettingChange: SubscriptionChange
|
userSettingChange: SubscriptionChange
|
||||||
|
userLogout: SubscriptionChange
|
||||||
|
|
||||||
domainChange: SubscriptionChange
|
domainChange: SubscriptionChange
|
||||||
groupChange: SubscriptionChange
|
groupChange: SubscriptionChange
|
||||||
|
22
api/src/api_graphql/queries/domain_history_query.py
Normal file
22
api/src/api_graphql/queries/domain_history_query.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
|
||||||
|
from data.schemas.public.domain import Domain
|
||||||
|
from data.schemas.public.short_url import ShortUrl
|
||||||
|
from data.schemas.public.short_url_dao import shortUrlDao
|
||||||
|
|
||||||
|
|
||||||
|
class DomainHistoryQuery(DbHistoryModelQueryABC):
|
||||||
|
def __init__(self):
|
||||||
|
DbHistoryModelQueryABC.__init__(self, "Domain")
|
||||||
|
|
||||||
|
self.set_field("name", lambda x, *_: x.name)
|
||||||
|
self.set_field("shortUrls", self._get_urls)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _get_urls(domain: Domain, *_):
|
||||||
|
return await shortUrlDao.find_by(
|
||||||
|
[
|
||||||
|
{ShortUrl.domain_id: domain.id},
|
||||||
|
{ShortUrl.deleted: False},
|
||||||
|
{"updated": {"lessOrEqual": domain.updated}},
|
||||||
|
]
|
||||||
|
)
|
@ -1,5 +1,6 @@
|
|||||||
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
||||||
from data.schemas.public.domain import Domain
|
from data.schemas.public.domain import Domain
|
||||||
|
from data.schemas.public.domain_dao import domainDao
|
||||||
from data.schemas.public.group import Group
|
from data.schemas.public.group import Group
|
||||||
from data.schemas.public.short_url import ShortUrl
|
from data.schemas.public.short_url import ShortUrl
|
||||||
from data.schemas.public.short_url_dao import shortUrlDao
|
from data.schemas.public.short_url_dao import shortUrlDao
|
||||||
@ -7,11 +8,13 @@ from data.schemas.public.short_url_dao import shortUrlDao
|
|||||||
|
|
||||||
class DomainQuery(DbModelQueryABC):
|
class DomainQuery(DbModelQueryABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DbModelQueryABC.__init__(self, "Domain")
|
DbModelQueryABC.__init__(self, "Domain", domainDao, with_history=True)
|
||||||
|
|
||||||
self.set_field("name", lambda x, *_: x.name)
|
self.set_field("name", lambda x, *_: x.name)
|
||||||
self.set_field("shortUrls", self._get_urls)
|
self.set_field("shortUrls", self._get_urls)
|
||||||
|
|
||||||
|
self.set_history_reference_dao(shortUrlDao, "domainid")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_urls(domain: Domain, *_):
|
async def _get_urls(domain: Domain, *_):
|
||||||
return await shortUrlDao.find_by({ShortUrl.domain_id: domain.id})
|
return await shortUrlDao.find_by({ShortUrl.domain_id: domain.id})
|
||||||
|
47
api/src/api_graphql/queries/group_history_query.py
Normal file
47
api/src/api_graphql/queries/group_history_query.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
|
||||||
|
from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
|
||||||
|
from api_graphql.require_any_resolvers import group_by_assignment_resolver
|
||||||
|
from data.schemas.public.group import Group
|
||||||
|
from data.schemas.public.group_dao import groupDao
|
||||||
|
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
|
||||||
|
from data.schemas.public.short_url import ShortUrl
|
||||||
|
from data.schemas.public.short_url_dao import shortUrlDao
|
||||||
|
from service.permission.permissions_enum import Permissions
|
||||||
|
|
||||||
|
|
||||||
|
class GroupHistoryQuery(DbHistoryModelQueryABC):
|
||||||
|
def __init__(self):
|
||||||
|
DbHistoryModelQueryABC.__init__(self, "Group")
|
||||||
|
|
||||||
|
self.set_field("name", lambda x, *_: x.name)
|
||||||
|
self.field(
|
||||||
|
ResolverFieldBuilder("shortUrls")
|
||||||
|
.with_resolver(self._get_urls)
|
||||||
|
.with_require_any(
|
||||||
|
[
|
||||||
|
Permissions.groups,
|
||||||
|
],
|
||||||
|
[group_by_assignment_resolver],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.set_field(
|
||||||
|
"roles",
|
||||||
|
lambda x, *_: self._resolve_foreign_history(
|
||||||
|
x.updated,
|
||||||
|
x.id,
|
||||||
|
groupRoleAssignmentDao,
|
||||||
|
groupDao,
|
||||||
|
lambda y: y.role_id,
|
||||||
|
obj_key="groupid",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _get_urls(group: Group, *_):
|
||||||
|
return await shortUrlDao.find_by(
|
||||||
|
[
|
||||||
|
{ShortUrl.group_id: group.id},
|
||||||
|
{ShortUrl.deleted: False},
|
||||||
|
{"updated": {"lessOrEqual": group.updated}},
|
||||||
|
]
|
||||||
|
)
|
@ -3,6 +3,7 @@ from api_graphql.field.resolver_field_builder import ResolverFieldBuilder
|
|||||||
from api_graphql.require_any_resolvers import group_by_assignment_resolver
|
from api_graphql.require_any_resolvers import group_by_assignment_resolver
|
||||||
from data.schemas.public.group import Group
|
from data.schemas.public.group import Group
|
||||||
from data.schemas.public.group_dao import groupDao
|
from data.schemas.public.group_dao import groupDao
|
||||||
|
from data.schemas.public.group_role_assignment_dao import groupRoleAssignmentDao
|
||||||
from data.schemas.public.short_url import ShortUrl
|
from data.schemas.public.short_url import ShortUrl
|
||||||
from data.schemas.public.short_url_dao import shortUrlDao
|
from data.schemas.public.short_url_dao import shortUrlDao
|
||||||
from service.permission.permissions_enum import Permissions
|
from service.permission.permissions_enum import Permissions
|
||||||
@ -10,7 +11,7 @@ from service.permission.permissions_enum import Permissions
|
|||||||
|
|
||||||
class GroupQuery(DbModelQueryABC):
|
class GroupQuery(DbModelQueryABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DbModelQueryABC.__init__(self, "Group")
|
DbModelQueryABC.__init__(self, "Group", groupDao, with_history=True)
|
||||||
|
|
||||||
self.set_field("name", lambda x, *_: x.name)
|
self.set_field("name", lambda x, *_: x.name)
|
||||||
self.field(
|
self.field(
|
||||||
@ -25,6 +26,9 @@ class GroupQuery(DbModelQueryABC):
|
|||||||
)
|
)
|
||||||
self.set_field("roles", self._get_roles)
|
self.set_field("roles", self._get_roles)
|
||||||
|
|
||||||
|
self.set_history_reference_dao(shortUrlDao, "groupid")
|
||||||
|
self.set_history_reference_dao(groupRoleAssignmentDao, "groupid")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _get_urls(group: Group, *_):
|
async def _get_urls(group: Group, *_):
|
||||||
return await shortUrlDao.find_by({ShortUrl.group_id: group.id})
|
return await shortUrlDao.find_by({ShortUrl.group_id: group.id})
|
||||||
|
14
api/src/api_graphql/queries/short_url_history_query.py
Normal file
14
api/src/api_graphql/queries/short_url_history_query.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from api_graphql.abc.db_history_model_query_abc import DbHistoryModelQueryABC
|
||||||
|
|
||||||
|
|
||||||
|
class ShortUrlQuery(DbHistoryModelQueryABC):
|
||||||
|
def __init__(self):
|
||||||
|
DbHistoryModelQueryABC.__init__(self, "ShortUrl")
|
||||||
|
|
||||||
|
self.set_field("shortUrl", lambda x, *_: x.short_url)
|
||||||
|
self.set_field("targetUrl", lambda x, *_: x.target_url)
|
||||||
|
self.set_field("description", lambda x, *_: x.description)
|
||||||
|
self.set_field("group", lambda x, *_: x.group)
|
||||||
|
self.set_field("domain", lambda x, *_: x.domain)
|
||||||
|
self.set_field("visits", lambda x, *_: x.visit_count)
|
||||||
|
self.set_field("loadingScreen", lambda x, *_: x.loading_screen)
|
@ -1,9 +1,10 @@
|
|||||||
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
from api_graphql.abc.db_model_query_abc import DbModelQueryABC
|
||||||
|
from data.schemas.public.short_url_dao import shortUrlDao
|
||||||
|
|
||||||
|
|
||||||
class ShortUrlQuery(DbModelQueryABC):
|
class ShortUrlQuery(DbModelQueryABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DbModelQueryABC.__init__(self, "ShortUrl")
|
DbModelQueryABC.__init__(self, "ShortUrl", shortUrlDao, with_history=True)
|
||||||
|
|
||||||
self.set_field("shortUrl", lambda x, *_: x.short_url)
|
self.set_field("shortUrl", lambda x, *_: x.short_url)
|
||||||
self.set_field("targetUrl", lambda x, *_: x.target_url)
|
self.set_field("targetUrl", lambda x, *_: x.target_url)
|
||||||
|
@ -49,6 +49,12 @@ class Subscription(SubscriptionABC):
|
|||||||
.with_public(True)
|
.with_public(True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.subscribe(
|
||||||
|
SubscriptionFieldBuilder("userLogout")
|
||||||
|
.with_resolver(lambda message, *_: message.message)
|
||||||
|
.with_public(True)
|
||||||
|
)
|
||||||
|
|
||||||
self.subscribe(
|
self.subscribe(
|
||||||
SubscriptionFieldBuilder("domainChange")
|
SubscriptionFieldBuilder("domainChange")
|
||||||
.with_resolver(lambda message, *_: message.message)
|
.with_resolver(lambda message, *_: message.message)
|
||||||
|
@ -111,7 +111,11 @@ def _find_short_url_by_path(path: str) -> Optional[dict]:
|
|||||||
if "errors" in data:
|
if "errors" in data:
|
||||||
logger.warning(f"Failed to find short url by path {path} -> {data["errors"]}")
|
logger.warning(f"Failed to find short url by path {path} -> {data["errors"]}")
|
||||||
|
|
||||||
if "data" not in data or "shortUrls" not in data["data"] or "nodes" not in data["data"]["shortUrls"]:
|
if (
|
||||||
|
"data" not in data
|
||||||
|
or "shortUrls" not in data["data"]
|
||||||
|
or "nodes" not in data["data"]["shortUrls"]
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = data["data"]["shortUrls"]["nodes"]
|
data = data["data"]["shortUrls"]["nodes"]
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
<main *ngIf="isLoggedIn && !hideUI; else home" [class]="theme">
|
<main [class]="theme">
|
||||||
|
<div
|
||||||
|
class="warning bg3 flex justify-center p-1.5"
|
||||||
|
*ngIf="showTechnicalDemoBanner">
|
||||||
|
{{ 'technical_demo_banner' | translate }}
|
||||||
|
</div>
|
||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<aside *ngIf="showSidebar">
|
<aside *ngIf="showSidebar">
|
||||||
<app-sidebar></app-sidebar>
|
<app-sidebar></app-sidebar>
|
||||||
</aside>
|
</aside>
|
||||||
<section class="component">
|
<section class="component" *ngIf="loadedGuiSettings">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -30,7 +35,3 @@
|
|||||||
</p-confirmDialog>
|
</p-confirmDialog>
|
||||||
</main>
|
</main>
|
||||||
<app-spinner></app-spinner>
|
<app-spinner></app-spinner>
|
||||||
|
|
||||||
<ng-template #home>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</ng-template>
|
|
@ -2,8 +2,8 @@ import { Component, OnDestroy } from '@angular/core';
|
|||||||
import { SidebarService } from 'src/app/service/sidebar.service';
|
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { AuthService } from 'src/app/service/auth.service';
|
|
||||||
import { GuiService } from 'src/app/service/gui.service';
|
import { GuiService } from 'src/app/service/gui.service';
|
||||||
|
import { FeatureFlagService } from 'src/app/service/feature-flag.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -11,36 +11,35 @@ import { GuiService } from 'src/app/service/gui.service';
|
|||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnDestroy {
|
export class AppComponent implements OnDestroy {
|
||||||
theme = 'open-redirect';
|
|
||||||
showSidebar = false;
|
showSidebar = false;
|
||||||
hideUI = false;
|
theme = 'lan-maestro';
|
||||||
isLoggedIn = false;
|
showTechnicalDemoBanner = false;
|
||||||
|
|
||||||
|
loadedGuiSettings = false;
|
||||||
unsubscribe$ = new Subject<void>();
|
unsubscribe$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private sidebar: SidebarService,
|
private sidebar: SidebarService,
|
||||||
private auth: AuthService,
|
private gui: GuiService,
|
||||||
private gui: GuiService
|
private features: FeatureFlagService
|
||||||
) {
|
) {
|
||||||
this.auth.loadUser();
|
this.features.get('TechnicalDemoBanner').then(showTechnicalDemoBanner => {
|
||||||
|
this.showTechnicalDemoBanner = showTechnicalDemoBanner;
|
||||||
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe(user => {
|
|
||||||
this.isLoggedIn = user !== null && user !== undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sidebar.visible$
|
this.sidebar.visible$
|
||||||
.pipe(takeUntil(this.unsubscribe$))
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
.subscribe(visible => {
|
.subscribe(visible => {
|
||||||
this.showSidebar = visible;
|
this.showSidebar = visible;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gui.hideGui$.pipe(takeUntil(this.unsubscribe$)).subscribe(hide => {
|
|
||||||
this.hideUI = hide;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.gui.theme$.pipe(takeUntil(this.unsubscribe$)).subscribe(theme => {
|
this.gui.theme$.pipe(takeUntil(this.unsubscribe$)).subscribe(theme => {
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
});
|
});
|
||||||
|
this.gui.loadedGuiSettings$
|
||||||
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
|
.subscribe(loaded => {
|
||||||
|
this.loadedGuiSettings = loaded;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
import {
|
||||||
|
APP_INITIALIZER,
|
||||||
|
ApplicationRef,
|
||||||
|
DoBootstrap,
|
||||||
|
ErrorHandler,
|
||||||
|
Injector,
|
||||||
|
NgModule,
|
||||||
|
} from '@angular/core';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@ -23,6 +30,8 @@ import { SidebarComponent } from './components/sidebar/sidebar.component';
|
|||||||
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
import { ConfigService } from 'src/app/service/config.service';
|
import { ConfigService } from 'src/app/service/config.service';
|
||||||
import { ServerUnavailableComponent } from 'src/app/components/error/server-unavailable/server-unavailable.component';
|
import { ServerUnavailableComponent } from 'src/app/components/error/server-unavailable/server-unavailable.component';
|
||||||
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
Logger.enableProductionMode();
|
Logger.enableProductionMode();
|
||||||
@ -95,6 +104,20 @@ export function appInitializerFactory(
|
|||||||
useClass: ErrorHandlingService,
|
useClass: ErrorHandlingService,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule implements DoBootstrap {
|
||||||
|
constructor(private injector: Injector) {}
|
||||||
|
|
||||||
|
async ngDoBootstrap(appRef: ApplicationRef) {
|
||||||
|
const spinner = this.injector.get(SpinnerService);
|
||||||
|
spinner.show();
|
||||||
|
|
||||||
|
const auth = this.injector.get(AuthService);
|
||||||
|
const user = await auth.loadUser();
|
||||||
|
if (!user) {
|
||||||
|
await auth.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
appRef.bootstrap(AppComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { ErrorComponentBase } from 'src/app/core/base/error-component-base';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-not-found',
|
selector: 'app-not-found',
|
||||||
templateUrl: './not-found.component.html',
|
templateUrl: './not-found.component.html',
|
||||||
styleUrls: ['./not-found.component.scss'],
|
styleUrls: ['./not-found.component.scss'],
|
||||||
})
|
})
|
||||||
export class NotFoundComponent {}
|
export class NotFoundComponent extends ErrorComponentBase {}
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { ErrorComponentBase } from 'src/app/core/base/error-component-base';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-server-unavailable',
|
selector: 'app-server-unavailable',
|
||||||
templateUrl: './server-unavailable.component.html',
|
templateUrl: './server-unavailable.component.html',
|
||||||
styleUrls: ['./server-unavailable.component.scss'],
|
styleUrls: ['./server-unavailable.component.scss'],
|
||||||
})
|
})
|
||||||
export class ServerUnavailableComponent {
|
export class ServerUnavailableComponent extends ErrorComponentBase {
|
||||||
constructor(private router: Router) {}
|
constructor(private router: Router) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
async retryConnection() {
|
async retryConnection() {
|
||||||
await this.router.navigate(['/']);
|
await this.router.navigate(['/']);
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
<footer>
|
<footer class="flex justify-between pl-1 pr-1">
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<span>web: {{ webVersion }}</span>
|
||||||
|
<span class="divider"> | </span>
|
||||||
|
<span>api: {{ apiVersion }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<a [href]="termsUrl">{{ 'footer.terms' | translate }}</a>
|
<a [href]="termsUrl">{{ 'footer.terms' | translate }}</a>
|
||||||
<span class="divider"> | </span>
|
<span class="divider"> | </span>
|
||||||
<a [href]="privacyUrl">{{ 'footer.privacy' | translate }}</a>
|
<a [href]="privacyUrl">{{ 'footer.privacy' | translate }}</a>
|
||||||
<span class="divider"> | </span>
|
<span class="divider"> | </span>
|
||||||
<a [href]="imprintUrl">{{ 'footer.imprint' | translate }}</a>
|
<a [href]="imprintUrl">{{ 'footer.imprint' | translate }}</a>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
@import "../../../styles/constants.scss";
|
|
||||||
|
|
||||||
footer {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 25px;
|
|
||||||
padding: 0 5px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,37 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { FooterComponent } from "src/app/components/footer/footer.component";
|
import { FooterComponent } from 'src/app/components/footer/footer.component';
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { BrowserModule } from "@angular/platform-browser";
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { SharedModule } from "src/app/modules/shared/shared.module";
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
describe("FooterComponent", () => {
|
describe('FooterComponent', () => {
|
||||||
let component: FooterComponent;
|
let component: FooterComponent;
|
||||||
let fixture: ComponentFixture<FooterComponent>;
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [FooterComponent],
|
declarations: [FooterComponent],
|
||||||
imports: [
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
BrowserModule,
|
providers: [
|
||||||
BrowserAnimationsModule,
|
AuthService,
|
||||||
SharedModule,
|
KeycloakService,
|
||||||
TranslateModule.forRoot(),
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: of({}) },
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@ -28,7 +42,7 @@ describe("FooterComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ConfigService } from 'src/app/service/config.service';
|
import { ConfigService } from 'src/app/service/config.service';
|
||||||
|
import { VersionService } from 'src/app/service/version.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
@ -7,7 +9,21 @@ import { ConfigService } from 'src/app/service/config.service';
|
|||||||
styleUrls: ['./footer.component.scss'],
|
styleUrls: ['./footer.component.scss'],
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
constructor(private config: ConfigService) {}
|
webVersion = '0.0.0';
|
||||||
|
apiVersion = '0.0.0';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private toast: ToastService,
|
||||||
|
private config: ConfigService,
|
||||||
|
private version: VersionService
|
||||||
|
) {
|
||||||
|
this.version.getApiVersion().subscribe(version => {
|
||||||
|
this.apiVersion = version;
|
||||||
|
});
|
||||||
|
this.version.getWebVersion().subscribe(version => {
|
||||||
|
this.webVersion = version.version;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get termsUrl(): string {
|
get termsUrl(): string {
|
||||||
return this.config.settings.termsUrl;
|
return this.config.settings.termsUrl;
|
||||||
|
@ -2,22 +2,25 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<p-button
|
<p-button
|
||||||
*ngIf="user"
|
type="button"
|
||||||
icon="pi pi-bars"
|
icon="pi pi-bars"
|
||||||
class="btn icon-btn p-button-text"
|
class="btn icon-btn p-button-text"
|
||||||
(onClick)="toggleSidebar()"
|
(onClick)="toggleSidebar()"></p-button>
|
||||||
></p-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<!-- <img src="/assets/images/logo.svg" alt="logo"/>-->
|
<!-- <img src="/assets/images/logo.svg" alt="logo"/>-->
|
||||||
</div>
|
</div>
|
||||||
<div class="app-name">
|
<div class="app-name">
|
||||||
<h1>Open-redirect</h1>
|
<h1>LAN-Maestro</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center w-1/3" *ngIf="menu.length > 0">
|
||||||
|
<app-menu-bar class="w-full" [elements]="menu"></app-menu-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center" *ngIf="themeList.length > 0">
|
||||||
<p-button
|
<p-button
|
||||||
type="button"
|
type="button"
|
||||||
icon="pi pi-palette"
|
icon="pi pi-palette"
|
||||||
@ -29,7 +32,7 @@
|
|||||||
[model]="themeList"
|
[model]="themeList"
|
||||||
class="lang-menu"></p-menu>
|
class="lang-menu"></p-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center" *ngIf="langList.length > 0">
|
||||||
<p-button
|
<p-button
|
||||||
type="button"
|
type="button"
|
||||||
icon="pi pi-globe"
|
icon="pi pi-globe"
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { HeaderComponent } from "src/app/components/header/header.component";
|
import { HeaderComponent } from 'src/app/components/header/header.component';
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ConfirmationService, MessageService } from "primeng/api";
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { of } from "rxjs";
|
import { of } from 'rxjs';
|
||||||
import { SharedModule } from "src/app/modules/shared/shared.module";
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
import { ErrorHandlingService } from "src/app/service/error-handling.service";
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
import { ToastService } from "src/app/service/toast.service";
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { AuthService } from "src/app/service/auth.service";
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { KeycloakService } from "keycloak-angular";
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
|
||||||
describe("HeaderComponent", () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ describe("HeaderComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -48,12 +48,12 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe(async user => {
|
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe(async user => {
|
||||||
this.user = user;
|
|
||||||
await this.initMenuLists();
|
await this.initMenuLists();
|
||||||
if (user) {
|
|
||||||
await this.loadTheme();
|
await this.loadTheme();
|
||||||
await this.loadLang();
|
await this.loadLang();
|
||||||
}
|
|
||||||
|
this.user = user;
|
||||||
|
this.guiService.loadedGuiSettings$.next(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.themeList = this.config.settings.themes.map(theme => {
|
this.themeList = this.config.settings.themes.map(theme => {
|
||||||
@ -87,27 +87,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initMenuList() {
|
async initMenuList() {
|
||||||
this.menu = [
|
this.menu = [];
|
||||||
{
|
|
||||||
label: 'common.news',
|
|
||||||
routerLink: ['/'],
|
|
||||||
icon: 'pi pi-home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'header.menu.about',
|
|
||||||
routerLink: ['/about'],
|
|
||||||
icon: 'pi pi-info',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (this.auth.user$.value) {
|
|
||||||
this.menu.push({
|
|
||||||
label: 'header.menu.admin',
|
|
||||||
routerLink: ['/admin'],
|
|
||||||
icon: 'pi pi-cog',
|
|
||||||
visible: await this.auth.isAdmin(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initLangMenuList() {
|
async initLangMenuList() {
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<p>home works!</p>
|
@ -1,15 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { HomeComponent } from './home.component';
|
import { HomeComponent } from './home.component';
|
||||||
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { AuthService } from 'src/app/service/auth.service';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
|
||||||
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
|
||||||
import { ToastService } from 'src/app/service/toast.service';
|
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
|
|
||||||
describe('HomeComponent', () => {
|
describe('HomeComponent', () => {
|
||||||
let component: HomeComponent;
|
let component: HomeComponent;
|
||||||
@ -18,21 +9,6 @@ describe('HomeComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [HomeComponent],
|
declarations: [HomeComponent],
|
||||||
imports: [SharedModule, TranslateModule.forRoot()],
|
|
||||||
providers: [
|
|
||||||
AuthService,
|
|
||||||
KeycloakService,
|
|
||||||
ErrorHandlingService,
|
|
||||||
ToastService,
|
|
||||||
MessageService,
|
|
||||||
ConfirmationService,
|
|
||||||
{
|
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: {
|
|
||||||
snapshot: { params: of({}) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(HomeComponent);
|
fixture = TestBed.createComponent(HomeComponent);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
@ -7,9 +7,7 @@ import { KeycloakService } from 'keycloak-angular';
|
|||||||
styleUrl: './home.component.scss',
|
styleUrl: './home.component.scss',
|
||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent {
|
||||||
constructor(private keycloak: KeycloakService) {
|
constructor(private spinner: SpinnerService) {
|
||||||
if (!this.keycloak.isLoggedIn()) {
|
this.spinner.hide();
|
||||||
this.keycloak.login().then(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { SidebarComponent } from "./sidebar.component";
|
import { SidebarComponent } from './sidebar.component';
|
||||||
import { SharedModule } from "src/app/modules/shared/shared.module";
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { AuthService } from "src/app/service/auth.service";
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { ErrorHandlingService } from "src/app/service/error-handling.service";
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
import { ToastService } from "src/app/service/toast.service";
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { ConfirmationService, MessageService } from "primeng/api";
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { of } from "rxjs";
|
import { of } from 'rxjs';
|
||||||
import { KeycloakService } from "keycloak-angular";
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
|
||||||
describe("SidebarComponent", () => {
|
describe('SidebarComponent', () => {
|
||||||
let component: SidebarComponent;
|
let component: SidebarComponent;
|
||||||
let fixture: ComponentFixture<SidebarComponent>;
|
let fixture: ComponentFixture<SidebarComponent>;
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ describe("SidebarComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Component, OnDestroy } from "@angular/core";
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
import { MenuElement } from "src/app/model/view/menu-element";
|
import { MenuElement } from 'src/app/model/view/menu-element';
|
||||||
import { Subject } from "rxjs";
|
import { Subject } from 'rxjs';
|
||||||
import { SidebarService } from "src/app/service/sidebar.service";
|
import { SidebarService } from 'src/app/service/sidebar.service';
|
||||||
import { takeUntil } from "rxjs/operators";
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-sidebar",
|
selector: 'app-sidebar',
|
||||||
templateUrl: "./sidebar.component.html",
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrl: "./sidebar.component.scss",
|
styleUrl: './sidebar.component.scss',
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnDestroy {
|
export class SidebarComponent implements OnDestroy {
|
||||||
elements: MenuElement[] = [];
|
elements: MenuElement[] = [];
|
||||||
@ -17,7 +17,7 @@ export class SidebarComponent implements OnDestroy {
|
|||||||
constructor(private sidebar: SidebarService) {
|
constructor(private sidebar: SidebarService) {
|
||||||
this.sidebar.elements$
|
this.sidebar.elements$
|
||||||
.pipe(takeUntil(this.unsubscribe$))
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
.subscribe((elements) => {
|
.subscribe(elements => {
|
||||||
this.elements = elements;
|
this.elements = elements;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { SpinnerComponent } from "src/app/components/spinner/spinner.component";
|
import { SpinnerComponent } from 'src/app/components/spinner/spinner.component';
|
||||||
|
|
||||||
describe("SpinnerComponent", () => {
|
describe('SpinnerComponent', () => {
|
||||||
let component: SpinnerComponent;
|
let component: SpinnerComponent;
|
||||||
let fixture: ComponentFixture<SpinnerComponent>;
|
let fixture: ComponentFixture<SpinnerComponent>;
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ describe("SpinnerComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from '@angular/core';
|
||||||
import { SpinnerService } from "src/app/service/spinner.service";
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-spinner",
|
selector: 'app-spinner',
|
||||||
templateUrl: "./spinner.component.html",
|
templateUrl: './spinner.component.html',
|
||||||
styleUrls: ["./spinner.component.scss"],
|
styleUrls: ['./spinner.component.scss'],
|
||||||
})
|
})
|
||||||
export class SpinnerComponent {
|
export class SpinnerComponent {
|
||||||
showSpinnerState = false;
|
showSpinnerState = false;
|
||||||
|
|
||||||
constructor(public spinnerService: SpinnerService) {
|
constructor(public spinnerService: SpinnerService) {
|
||||||
this.spinnerService.showSpinnerState$.subscribe((value) => {
|
this.spinnerService.showSpinnerState$.subscribe(value => {
|
||||||
this.showSpinnerState = value;
|
this.showSpinnerState = value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
9
web/src/app/core/base/error-component-base.ts
Normal file
9
web/src/app/core/base/error-component-base.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
|
||||||
|
export class ErrorComponentBase {
|
||||||
|
constructor() {
|
||||||
|
const spinner = inject(SpinnerService);
|
||||||
|
spinner.hide();
|
||||||
|
}
|
||||||
|
}
|
@ -26,8 +26,8 @@ export abstract class FormPageBase<
|
|||||||
protected filterService = inject(FilterService);
|
protected filterService = inject(FilterService);
|
||||||
protected dataService = inject(PageDataService) as S;
|
protected dataService = inject(PageDataService) as S;
|
||||||
|
|
||||||
protected constructor() {
|
protected constructor(idKey: string = 'id') {
|
||||||
const id = this.route.snapshot.params['id'];
|
const id = this.route.snapshot.params[idKey];
|
||||||
this.validateRoute(id);
|
this.validateRoute(id);
|
||||||
|
|
||||||
this.buildForm();
|
this.buildForm();
|
||||||
|
@ -115,7 +115,6 @@ export abstract class PageBase<
|
|||||||
.onChange()
|
.onChange()
|
||||||
.pipe(takeUntil(this.unsubscribe$))
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
logger.debug('Reload data');
|
|
||||||
this.load(true);
|
this.load(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
49
web/src/app/core/base/page-with-history.data.service.ts
Normal file
49
web/src/app/core/base/page-with-history.data.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { MutationResult } from 'apollo-angular';
|
||||||
|
import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
||||||
|
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
||||||
|
import { QueryResult } from 'src/app/model/entities/query-result';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export abstract class PageWithHistoryDataService<T> {
|
||||||
|
abstract load(
|
||||||
|
filter?: Filter[],
|
||||||
|
sort?: Sort[],
|
||||||
|
skip?: number,
|
||||||
|
take?: number
|
||||||
|
): Observable<QueryResult<T>>;
|
||||||
|
|
||||||
|
abstract loadHistory(id: number, options?: object): Observable<DbModel[]>;
|
||||||
|
|
||||||
|
abstract onChange(): Observable<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Create<T, C> {
|
||||||
|
create(object: C): Observable<T | undefined> | Observable<MutationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Update<T, U> {
|
||||||
|
update(object: U): Observable<T | undefined> | Observable<MutationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Delete<T> {
|
||||||
|
delete(
|
||||||
|
object: T
|
||||||
|
): Observable<T | undefined | boolean> | Observable<MutationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Restore<T> {
|
||||||
|
restore(
|
||||||
|
object: T
|
||||||
|
): Observable<T | undefined | boolean> | Observable<MutationResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadHistory<T> {
|
||||||
|
loadHistory(
|
||||||
|
id: number
|
||||||
|
): Observable<T | undefined | boolean> | Observable<DbModel[] | undefined>;
|
||||||
|
}
|
@ -14,6 +14,7 @@ export const ID_COLUMN = {
|
|||||||
translationKey: 'common.id',
|
translationKey: 'common.id',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
value: (row: { id?: number }) => row.id,
|
value: (row: { id?: number }) => row.id,
|
||||||
class: 'max-w-24',
|
class: 'max-w-24',
|
||||||
};
|
};
|
||||||
@ -23,6 +24,7 @@ export const NAME_COLUMN = {
|
|||||||
translationKey: 'common.name',
|
translationKey: 'common.name',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
value: (row: { name?: string }) => row.name,
|
value: (row: { name?: string }) => row.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export const DESCRIPTION_COLUMN = {
|
|||||||
translationKey: 'common.description',
|
translationKey: 'common.description',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
value: (row: { description?: string }) => row.description,
|
value: (row: { description?: string }) => row.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,35 +41,49 @@ export const DELETED_COLUMN = {
|
|||||||
name: 'deleted',
|
name: 'deleted',
|
||||||
translationKey: 'common.deleted',
|
translationKey: 'common.deleted',
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
filterable: true,
|
filterable: false,
|
||||||
|
sortable: true,
|
||||||
value: (row: DbModel) => row.deleted,
|
value: (row: DbModel) => row.deleted,
|
||||||
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EDITOR_COLUMN = {
|
export const EDITOR_COLUMN = {
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
translationKey: 'common.editor',
|
translationKey: 'common.editor',
|
||||||
|
type: 'text',
|
||||||
|
filterable: true,
|
||||||
value: (row: DbModel) => row.editor?.username,
|
value: (row: DbModel) => row.editor?.username,
|
||||||
|
filterSelector: (mode: string, value: unknown) => {
|
||||||
|
return { editor: { username: { [mode]: value } } };
|
||||||
|
},
|
||||||
|
class: 'max-w-32',
|
||||||
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CREATED_UTC_COLUMN = {
|
export const CREATED_UTC_COLUMN = {
|
||||||
name: 'createdUtc',
|
name: 'created',
|
||||||
translationKey: 'common.created',
|
translationKey: 'common.created',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
value: (row: DbModel) => row.createdUtc,
|
sortable: true,
|
||||||
|
value: (row: DbModel) => row.created,
|
||||||
class: 'max-w-32',
|
class: 'max-w-32',
|
||||||
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UPDATED_UTC_COLUMN = {
|
export const UPDATED_UTC_COLUMN = {
|
||||||
name: 'updatedUtc',
|
name: 'updated',
|
||||||
translationKey: 'common.updated',
|
translationKey: 'common.updated',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
filterable: true,
|
filterable: true,
|
||||||
value: (row: DbModel) => row.updatedUtc,
|
sortable: true,
|
||||||
|
value: (row: DbModel) => row.updated,
|
||||||
class: 'max-w-32',
|
class: 'max-w-32',
|
||||||
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DB_MODEL_COLUMNS = [
|
export const DB_MODEL_COLUMNS = [
|
||||||
|
DELETED_COLUMN,
|
||||||
EDITOR_COLUMN,
|
EDITOR_COLUMN,
|
||||||
CREATED_UTC_COLUMN,
|
CREATED_UTC_COLUMN,
|
||||||
UPDATED_UTC_COLUMN,
|
UPDATED_UTC_COLUMN,
|
||||||
|
@ -1,22 +1,39 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from '@angular/core';
|
||||||
import { CanActivate } from "@angular/router";
|
import { CanActivate, Router } from '@angular/router';
|
||||||
import { KeycloakService } from "keycloak-angular";
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { Logger } from 'src/app/service/logger.service';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
|
||||||
|
const logger = new Logger('AuthGuard');
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(private keycloak: KeycloakService) {}
|
constructor(
|
||||||
|
private keycloak: KeycloakService,
|
||||||
|
private auth: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
async canActivate(): Promise<boolean> {
|
async canActivate(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!this.keycloak.isLoggedIn()) {
|
||||||
|
logger.debug('User not logged in, redirecting to login page');
|
||||||
|
await this.auth.login();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.keycloak.isTokenExpired()) {
|
if (this.keycloak.isTokenExpired()) {
|
||||||
|
logger.debug('Token expired, updating token');
|
||||||
await this.keycloak.updateToken();
|
await this.keycloak.updateToken();
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
if (!this.keycloak.isLoggedIn()) {
|
logger.error('Error during authentication', err);
|
||||||
await this.keycloak.login();
|
await this.router.navigate(['/']);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug('Check is user logged in');
|
||||||
return this.keycloak.isLoggedIn();
|
return this.keycloak.isLoggedIn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, Router } from "@angular/router";
|
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||||
import { Logger } from "src/app/service/logger.service";
|
import { Logger } from 'src/app/service/logger.service';
|
||||||
import { ToastService } from "src/app/service/toast.service";
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { AuthService } from "src/app/service/auth.service";
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
|
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||||
|
|
||||||
const log = new Logger("PermissionGuard");
|
const log = new Logger('PermissionGuard');
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class PermissionGuard {
|
export class PermissionGuard {
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private toast: ToastService,
|
private toast: ToastService,
|
||||||
private auth: AuthService,
|
private auth: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||||
const permissions = route.data["permissions"] as PermissionsEnum[];
|
const permissions = route.data['permissions'] as PermissionsEnum[];
|
||||||
|
|
||||||
if (!permissions || permissions.length === 0) {
|
if (!permissions || permissions.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
@ -26,11 +26,11 @@ export class PermissionGuard {
|
|||||||
|
|
||||||
const validate = await this.auth.hasAnyPermissionLazy(permissions);
|
const validate = await this.auth.hasAnyPermissionLazy(permissions);
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
log.debug("Permission denied", permissions);
|
log.debug('Permission denied', permissions);
|
||||||
this.toast.warn("common.warning", "error.permission_denied");
|
this.toast.warn('common.warning', 'error.permission_denied');
|
||||||
this.router.navigate(["/"]).then();
|
this.router.navigate(['/']).then();
|
||||||
}
|
}
|
||||||
log.debug("Permission granted", permissions);
|
log.debug('Permission granted', permissions);
|
||||||
return validate;
|
return validate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,55 @@ import { HttpInterceptorFn } from '@angular/common/http';
|
|||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { from, switchMap } from 'rxjs';
|
import { from, switchMap } from 'rxjs';
|
||||||
|
import { ConfigService } from 'src/app/service/config.service';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
|
||||||
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const keycloak = inject(KeycloakService);
|
const config = inject(ConfigService);
|
||||||
|
if (
|
||||||
|
!config.settings.api.url ||
|
||||||
|
!req.url.startsWith(config.settings.api.url)
|
||||||
|
) {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloak = inject(KeycloakService);
|
||||||
if (!keycloak.isLoggedIn()) {
|
if (!keycloak.isLoggedIn()) {
|
||||||
return next(req);
|
return next(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keycloak.isTokenExpired()) {
|
|
||||||
keycloak.updateToken().then();
|
|
||||||
}
|
|
||||||
|
|
||||||
return from(keycloak.getToken()).pipe(
|
return from(keycloak.getToken()).pipe(
|
||||||
switchMap(token => {
|
switchMap(token => {
|
||||||
const modifiedReq = token
|
if (!token) {
|
||||||
? req.clone({
|
return next(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keycloak.isTokenExpired()) {
|
||||||
|
return next(
|
||||||
|
req.clone({
|
||||||
headers: req.headers.set('Authorization', `Bearer ${token}`),
|
headers: req.headers.set('Authorization', `Bearer ${token}`),
|
||||||
})
|
})
|
||||||
: req;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return next(modifiedReq);
|
return from(keycloak.updateToken(30)).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
return keycloak.getToken();
|
||||||
|
}),
|
||||||
|
switchMap(newToken => {
|
||||||
|
return next(
|
||||||
|
req.clone({
|
||||||
|
headers: req.headers.set('Authorization', `Bearer ${newToken}`),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
const auth = inject(AuthService);
|
||||||
|
auth.logout().then();
|
||||||
|
return next(req);
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,10 @@ export enum PermissionsEnum {
|
|||||||
// Administration
|
// Administration
|
||||||
administrator = 'administrator',
|
administrator = 'administrator',
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings = 'settings',
|
||||||
|
settingsUpdate = 'settings.update',
|
||||||
|
|
||||||
apiKeys = 'api_keys',
|
apiKeys = 'api_keys',
|
||||||
apiKeysCreate = 'api_keys.create',
|
apiKeysCreate = 'api_keys.create',
|
||||||
apiKeysUpdate = 'api_keys.update',
|
apiKeysUpdate = 'api_keys.update',
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { Role } from "src/app/model/entities/role";
|
import { Role } from 'src/app/model/entities/role';
|
||||||
import { DbModel } from "src/app/model/entities/db-model";
|
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
|
||||||
|
|
||||||
export interface NotExistingUser {
|
export interface NotExistingUser {
|
||||||
keycloakId: string;
|
keycloakId: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User extends DbModel {
|
export interface User extends DbModelWithHistory {
|
||||||
id: number;
|
id: number;
|
||||||
|
keycloakId: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { DbModel } from 'src/app/model/entities/db-model';
|
import { DbModelWithHistory } from 'src/app/model/entities/db-model';
|
||||||
import { Permission } from 'src/app/model/entities/role';
|
import { Permission } from 'src/app/model/entities/role';
|
||||||
|
|
||||||
export interface ApiKey extends DbModel {
|
export interface ApiKey extends DbModelWithHistory {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
permissions?: Permission[];
|
permissions?: Permission[];
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import { User } from "src/app/model/auth/user";
|
import { User } from 'src/app/model/auth/user';
|
||||||
|
|
||||||
|
export interface DbModelWithHistory {
|
||||||
|
id?: number;
|
||||||
|
editor?: User;
|
||||||
|
deleted?: boolean;
|
||||||
|
created?: Date;
|
||||||
|
updated?: Date;
|
||||||
|
|
||||||
|
history?: DbModel[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface DbModel {
|
export interface DbModel {
|
||||||
id?: number;
|
id?: number;
|
||||||
editor?: User;
|
editor?: User;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
createdUtc?: Date;
|
created?: Date;
|
||||||
updatedUtc?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { DbModel } from "src/app/model/entities/db-model";
|
import { DbModel, DbModelWithHistory } from 'src/app/model/entities/db-model';
|
||||||
|
import { User } from 'src/app/model/auth/user';
|
||||||
|
|
||||||
export interface Role extends DbModel {
|
export interface Role extends DbModelWithHistory {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissions?: Permission[];
|
permissions?: Permission[];
|
||||||
|
users?: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoleCreateInput {
|
export interface RoleCreateInput {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { gql } from "apollo-angular";
|
import { gql } from 'apollo-angular';
|
||||||
import { EDITOR_FRAGMENT } from "src/app/model/graphql/editor.query";
|
import { EDITOR_FRAGMENT } from 'src/app/model/graphql/editor.query';
|
||||||
|
|
||||||
export const DB_MODEL_FRAGMENT = gql`
|
export const DB_MODEL_FRAGMENT = gql`
|
||||||
fragment DB_MODEL on DbModel {
|
fragment DB_MODEL on DbModel {
|
||||||
@ -10,9 +10,21 @@ export const DB_MODEL_FRAGMENT = gql`
|
|||||||
editor {
|
editor {
|
||||||
...EDITOR
|
...EDITOR
|
||||||
}
|
}
|
||||||
createdUtc
|
created
|
||||||
updatedUtc
|
updated
|
||||||
}
|
}
|
||||||
|
|
||||||
${EDITOR_FRAGMENT}
|
${EDITOR_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const DB_HISTORY_MODEL_FRAGMENT = gql`
|
||||||
|
fragment DB_HISTORY_MODEL on DbHistoryModel {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
|
||||||
|
deleted
|
||||||
|
editor
|
||||||
|
created
|
||||||
|
updated
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { gql } from "apollo-angular";
|
import { gql } from 'apollo-angular';
|
||||||
|
|
||||||
export const EDITOR_FRAGMENT = gql`
|
export const EDITOR_FRAGMENT = gql`
|
||||||
fragment EDITOR on User {
|
fragment EDITOR on User {
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
export type Sort = { [key: string]: any };
|
export type Sort = { [key: string]: any };
|
||||||
|
|
||||||
export enum SortOrder {
|
export enum SortOrder {
|
||||||
ASC = "ASC",
|
ASC = 'ASC',
|
||||||
DESC = "DESC",
|
DESC = 'DESC',
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ export interface MenuElement {
|
|||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
|
isSection?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export enum Themes {
|
export enum Themes {
|
||||||
Default = "maxlan-dark-theme",
|
Default = 'maxlan-dark-theme',
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,11 @@ import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
|||||||
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
redirectTo: 'users',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: 'users',
|
||||||
title: 'Users | Maxlan',
|
title: 'Users | Maxlan',
|
||||||
|
@ -12,7 +12,10 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
|||||||
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
||||||
import { Apollo, gql } from 'apollo-angular';
|
import { Apollo, gql } from 'apollo-angular';
|
||||||
import { QueryResult } from 'src/app/model/entities/query-result';
|
import { QueryResult } from 'src/app/model/entities/query-result';
|
||||||
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
|
import {
|
||||||
|
DB_HISTORY_MODEL_FRAGMENT,
|
||||||
|
DB_MODEL_FRAGMENT,
|
||||||
|
} from 'src/app/model/graphql/db-model.query';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { SpinnerService } from 'src/app/service/spinner.service';
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
import {
|
import {
|
||||||
@ -20,10 +23,12 @@ import {
|
|||||||
ApiKeyCreateInput,
|
ApiKeyCreateInput,
|
||||||
ApiKeyUpdateInput,
|
ApiKeyUpdateInput,
|
||||||
} from 'src/app/model/entities/api-key';
|
} from 'src/app/model/entities/api-key';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiKeysDataService
|
export class ApiKeysDataService
|
||||||
extends PageDataService<ApiKey>
|
extends PageWithHistoryDataService<ApiKey>
|
||||||
implements
|
implements
|
||||||
Create<ApiKey, ApiKeyCreateInput>,
|
Create<ApiKey, ApiKeyCreateInput>,
|
||||||
Update<ApiKey, ApiKeyUpdateInput>,
|
Update<ApiKey, ApiKeyUpdateInput>,
|
||||||
@ -91,8 +96,7 @@ export class ApiKeysDataService
|
|||||||
.query<{ apiKeys: QueryResult<ApiKey> }>({
|
.query<{ apiKeys: QueryResult<ApiKey> }>({
|
||||||
query: gql`
|
query: gql`
|
||||||
query getApiKey($id: Int) {
|
query getApiKey($id: Int) {
|
||||||
apiKeys(filter: { id: { equal: $id } }) {
|
apiKey(filter: { id: { equal: $id } }) {
|
||||||
nodes {
|
|
||||||
id
|
id
|
||||||
identifier
|
identifier
|
||||||
permissions {
|
permissions {
|
||||||
@ -102,7 +106,6 @@ export class ApiKeysDataService
|
|||||||
...DB_MODEL
|
...DB_MODEL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
${DB_MODEL_FRAGMENT}
|
${DB_MODEL_FRAGMENT}
|
||||||
`,
|
`,
|
||||||
@ -119,6 +122,42 @@ export class ApiKeysDataService
|
|||||||
.pipe(map(result => result.data.apiKeys.nodes[0]));
|
.pipe(map(result => result.data.apiKeys.nodes[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadHistory(id: number): Observable<DbModel[]> {
|
||||||
|
return this.apollo
|
||||||
|
.query<{ apiKeys: QueryResult<ApiKey> }>({
|
||||||
|
query: gql`
|
||||||
|
query getApiKeyHistory($id: Int) {
|
||||||
|
apiKeys(filter: { id: { equal: $id } }) {
|
||||||
|
count
|
||||||
|
totalCount
|
||||||
|
nodes {
|
||||||
|
history {
|
||||||
|
id
|
||||||
|
identifier
|
||||||
|
permissions {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
...DB_HISTORY_MODEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${DB_HISTORY_MODEL_FRAGMENT}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError(err => {
|
||||||
|
this.spinner.hide();
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(map(result => result.data?.apiKeys?.nodes?.[0]?.history ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
onChange(): Observable<void> {
|
onChange(): Observable<void> {
|
||||||
return this.apollo
|
return this.apollo
|
||||||
.subscribe<{ apiKeyChange: void }>({
|
.subscribe<{ apiKeyChange: void }>({
|
||||||
@ -208,7 +247,7 @@ export class ApiKeysDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ apiKey: { delete: boolean } }>({
|
.mutate<{ apiKey: { delete: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation deleteApiKey($id: ID!) {
|
mutation deleteApiKey($id: Int!) {
|
||||||
apiKey {
|
apiKey {
|
||||||
delete(id: $id)
|
delete(id: $id)
|
||||||
}
|
}
|
||||||
@ -231,7 +270,7 @@ export class ApiKeysDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ apiKey: { restore: boolean } }>({
|
.mutate<{ apiKey: { restore: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation restoreApiKey($id: ID!) {
|
mutation restoreApiKey($id: Int!) {
|
||||||
apiKey {
|
apiKey {
|
||||||
restore(id: $id)
|
restore(id: $id)
|
||||||
}
|
}
|
||||||
@ -255,7 +294,7 @@ export class ApiKeysDataService
|
|||||||
.query<{ permissions: QueryResult<Permission> }>({
|
.query<{ permissions: QueryResult<Permission> }>({
|
||||||
query: gql`
|
query: gql`
|
||||||
query getPermissions {
|
query getPermissions {
|
||||||
permissions {
|
permissions(sort: [{ name: ASC }]) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -279,6 +318,10 @@ export class ApiKeysDataService
|
|||||||
provide: PageDataService,
|
provide: PageDataService,
|
||||||
useClass: ApiKeysDataService,
|
useClass: ApiKeysDataService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PageWithHistoryDataService,
|
||||||
|
useClass: ApiKeysDataService,
|
||||||
|
},
|
||||||
ApiKeysDataService,
|
ApiKeysDataService,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { ApiKeyFormPageComponent } from 'src/app/modules/admin/administration/ap
|
|||||||
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page';
|
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page';
|
||||||
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
|
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
|
||||||
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns';
|
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns';
|
||||||
|
import { HistoryComponent } from 'src/app/modules/admin/administration/api-keys/history/history.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -31,12 +32,20 @@ const routes: Routes = [
|
|||||||
permissions: [PermissionsEnum.apiKeysUpdate],
|
permissions: [PermissionsEnum.apiKeysUpdate],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'history/:historyId',
|
||||||
|
component: HistoryComponent,
|
||||||
|
canActivate: [PermissionGuard],
|
||||||
|
data: {
|
||||||
|
permissions: [PermissionsEnum.apiKeys],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ApiKeysPage, ApiKeyFormPageComponent],
|
declarations: [ApiKeysPage, ApiKeyFormPageComponent, HistoryComponent],
|
||||||
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
||||||
providers: [ApiKeysDataService.provide(), ApiKeysColumns.provide()],
|
providers: [ApiKeysDataService.provide(), ApiKeysColumns.provide()],
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
[(skip)]="skip"
|
[(skip)]="skip"
|
||||||
[(take)]="take"
|
[(take)]="take"
|
||||||
(load)="load()"
|
(load)="load()"
|
||||||
|
[history]="true"
|
||||||
[create]="true"
|
[create]="true"
|
||||||
[update]="true"
|
[update]="true"
|
||||||
(delete)="delete($event)"
|
(delete)="delete($event)"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
(onClose)="close()">
|
(onClose)="close()">
|
||||||
<ng-template formPageHeader let-isUpdate>
|
<ng-template formPageHeader let-isUpdate>
|
||||||
<h2>
|
<h2>
|
||||||
{{ 'common.role' | translate }}
|
{{ 'common.api_key' | translate }}
|
||||||
{{
|
{{
|
||||||
(isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
|
(isUpdate ? 'sidebar.header.update' : 'sidebar.header.create')
|
||||||
| translate
|
| translate
|
||||||
@ -21,22 +21,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-page-input">
|
<div class="form-page-input">
|
||||||
<p class="label">{{ 'common.identifier' | translate }}</p>
|
<p class="label">{{ 'common.identifier' | translate }}</p>
|
||||||
<input pInputText class="value" type="text" formControlName="identifier"/>
|
<input
|
||||||
|
pInputText
|
||||||
|
class="value"
|
||||||
|
type="text"
|
||||||
|
formControlName="identifier" />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-5">
|
<div class="flex flex-col gap-5">
|
||||||
<div *ngFor="let group of Object.keys(permissionGroups)">
|
<div *ngFor="let group of Object.keys(permissionGroups)">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<label class="flex-1" for="roles.permission_groups.{{ group }}">
|
<label class="flex-1" for="apiKeys.permission_groups.{{ group }}">
|
||||||
<h3>
|
<h3>
|
||||||
{{ 'permissions.' + group | translate }}
|
{{ 'permissions.' + group | translate }}
|
||||||
</h3>
|
</h3>
|
||||||
</label>
|
</label>
|
||||||
<form #form="ngForm">
|
<form #form="ngForm">
|
||||||
<p-inputSwitch
|
<p-inputSwitch
|
||||||
name="roles.permission_groups.{{ group }}"
|
name="apiKeys.permission_groups.{{ group }}"
|
||||||
(onChange)="toggleGroup($event, group)"
|
(onChange)="toggleGroup($event, group)"
|
||||||
[ngModel]="isGroupChecked(group)"></p-inputSwitch>
|
[ngModel]="isGroupChecked(group)"></p-inputSwitch>
|
||||||
</form>
|
</form>
|
||||||
@ -44,20 +48,14 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
*ngFor="let permission of permissionGroups[group]"
|
*ngFor="let permission of permissionGroups[group]"
|
||||||
class="flex flex-col">
|
class="flex items-center justify-between w-full">
|
||||||
<div class="flex items-center justify-between w-full">
|
|
||||||
<label class="flex-1" for="{{ permission.name }}">
|
<label class="flex-1" for="{{ permission.name }}">
|
||||||
{{ 'permissions.' + permission.name | translate }}
|
{{ 'permissions.' + permission.name | translate }}
|
||||||
</label>
|
</label>
|
||||||
<p-inputSwitch class="flex items-center justify-center" [formControlName]="permission.name">
|
<p-inputSwitch [formControlName]="permission.name">
|
||||||
|
>
|
||||||
</p-inputSwitch>
|
</p-inputSwitch>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="permission.description">
|
|
||||||
<p class="text-sm text-gray-500">
|
|
||||||
{{ permission.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
ApiKeyUpdateInput,
|
ApiKeyUpdateInput,
|
||||||
} from 'src/app/model/entities/api-key';
|
} from 'src/app/model/entities/api-key';
|
||||||
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
|
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-api-key-form-page',
|
selector: 'app-api-key-form-page',
|
||||||
@ -27,10 +26,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
|
|||||||
permissionGroups: { [key: string]: Permission[] } = {};
|
permissionGroups: { [key: string]: Permission[] } = {};
|
||||||
allPermissions: Permission[] = [];
|
allPermissions: Permission[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(private toast: ToastService) {
|
||||||
private toast: ToastService,
|
|
||||||
private translate: TranslateService
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.initializePermissions().then(() => {
|
this.initializePermissions().then(() => {
|
||||||
if (!this.nodeId) {
|
if (!this.nodeId) {
|
||||||
@ -40,8 +36,10 @@ export class ApiKeyFormPageComponent extends FormPageBase<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dataService.loadById(this.nodeId).subscribe(apiKey => {
|
this.dataService
|
||||||
this.node = apiKey;
|
.load([{ id: { equal: this.nodeId } }])
|
||||||
|
.subscribe(apiKey => {
|
||||||
|
this.node = apiKey.nodes[0];
|
||||||
this.setForm(this.node);
|
this.setForm(this.node);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -51,12 +49,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
|
|||||||
const permissions = await firstValueFrom(
|
const permissions = await firstValueFrom(
|
||||||
this.dataService.getAllPermissions()
|
this.dataService.getAllPermissions()
|
||||||
);
|
);
|
||||||
this.allPermissions = permissions.map(x => {
|
this.allPermissions = permissions;
|
||||||
const key = `permission_descriptions.${x.name}`;
|
|
||||||
const description = this.translate.instant(key);
|
|
||||||
x.description = description === key ? undefined : description;
|
|
||||||
return x;
|
|
||||||
});
|
|
||||||
this.permissionGroups = permissions.reduce(
|
this.permissionGroups = permissions.reduce(
|
||||||
(acc, p) => {
|
(acc, p) => {
|
||||||
const group = p.name.includes('.') ? p.name.split('.')[0] : p.name;
|
const group = p.name.includes('.') ? p.name.split('.')[0] : p.name;
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<app-history-sidebar
|
||||||
|
[loadHistory]="loadHistory"
|
||||||
|
[columns]="columns"></app-history-sidebar>
|
@ -0,0 +1,65 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryComponent } from './history.component';
|
||||||
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { PageDataService } from 'src/app/core/base/page.data.service';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
|
||||||
|
|
||||||
|
describe('HistoryComponent', () => {
|
||||||
|
let component: HistoryComponent<unknown>;
|
||||||
|
let fixture: ComponentFixture<HistoryComponent<unknown>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [HistoryComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
KeycloakService,
|
||||||
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: { historyId: '3' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageDataService,
|
||||||
|
useClass: ApiKeysDataService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageColumns,
|
||||||
|
useClass: MockPageColumns,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
component.loadHistory = (id: number) => of([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history',
|
||||||
|
templateUrl: './history.component.html',
|
||||||
|
styleUrl: './history.component.scss',
|
||||||
|
})
|
||||||
|
export class HistoryComponent<T> {
|
||||||
|
loadHistory = (id: number) => this.data.loadHistory(id);
|
||||||
|
columns = this._columns.get();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public data: PageWithHistoryDataService<T>,
|
||||||
|
private _columns: PageColumns<T>
|
||||||
|
) {}
|
||||||
|
}
|
@ -5,6 +5,8 @@ import { SpinnerService } from 'src/app/service/spinner.service';
|
|||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
import { InputSwitchChangeEvent } from 'primeng/inputswitch';
|
import { InputSwitchChangeEvent } from 'primeng/inputswitch';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-feature-flags',
|
selector: 'app-feature-flags',
|
||||||
@ -18,10 +20,18 @@ export class FeatureFlagsPage implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private spinner: SpinnerService,
|
private spinner: SpinnerService,
|
||||||
private data: FeatureFlagsDataService
|
private data: FeatureFlagsDataService,
|
||||||
|
private auth: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const isAllowed = await this.auth.hasAnyPermissionLazy([
|
||||||
|
PermissionsEnum.administrator,
|
||||||
|
]);
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
this.data
|
this.data
|
||||||
.onUpdate()
|
.onUpdate()
|
||||||
.pipe(takeUntil(this.unsubscribe$))
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
@ -53,7 +63,14 @@ export class FeatureFlagsPage implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
change(flag: FeatureFlag, event: InputSwitchChangeEvent) {
|
async change(flag: FeatureFlag, event: InputSwitchChangeEvent) {
|
||||||
|
const isAllowed = await this.auth.hasAnyPermissionLazy([
|
||||||
|
PermissionsEnum.administrator,
|
||||||
|
]);
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
flag.value = event.checked;
|
flag.value = event.checked;
|
||||||
this.spinner.show();
|
this.spinner.show();
|
||||||
this.data
|
this.data
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<app-history-sidebar
|
||||||
|
[loadHistory]="loadHistory"
|
||||||
|
[columns]="columns"></app-history-sidebar>
|
@ -0,0 +1,65 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryComponent } from './history.component';
|
||||||
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { PageDataService } from 'src/app/core/base/page.data.service';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
|
||||||
|
|
||||||
|
describe('HistoryComponent', () => {
|
||||||
|
let component: HistoryComponent<unknown>;
|
||||||
|
let fixture: ComponentFixture<HistoryComponent<unknown>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [HistoryComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
KeycloakService,
|
||||||
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: { historyId: '3' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageDataService,
|
||||||
|
useClass: RolesDataService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageColumns,
|
||||||
|
useClass: MockPageColumns,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
component.loadHistory = (id: number) => of([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history',
|
||||||
|
templateUrl: './history.component.html',
|
||||||
|
styleUrl: './history.component.scss',
|
||||||
|
})
|
||||||
|
export class HistoryComponent<T> {
|
||||||
|
loadHistory = (id: number) => this.data.loadHistory(id);
|
||||||
|
columns = this._columns.get();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public data: PageWithHistoryDataService<T>,
|
||||||
|
private _columns: PageColumns<T>
|
||||||
|
) {}
|
||||||
|
}
|
@ -12,7 +12,23 @@ import { TableColumn } from 'src/app/modules/shared/components/table/table.model
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class RolesColumns extends PageColumns<Role> {
|
export class RolesColumns extends PageColumns<Role> {
|
||||||
get(): TableColumn<Role>[] {
|
get(): TableColumn<Role>[] {
|
||||||
return [ID_COLUMN, NAME_COLUMN, DESCRIPTION_COLUMN, ...DB_MODEL_COLUMNS];
|
return [
|
||||||
|
ID_COLUMN,
|
||||||
|
NAME_COLUMN,
|
||||||
|
DESCRIPTION_COLUMN,
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
translationKey: 'sidebar.users',
|
||||||
|
type: 'text',
|
||||||
|
filterable: false,
|
||||||
|
sortable: false,
|
||||||
|
value: (row: Role) =>
|
||||||
|
(row?.users ?? []).map(user => user.username).join(', '),
|
||||||
|
width: '300px',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
...DB_MODEL_COLUMNS,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
static provide(): Provider[] {
|
static provide(): Provider[] {
|
||||||
|
@ -17,13 +17,18 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
|||||||
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
||||||
import { Apollo, gql } from 'apollo-angular';
|
import { Apollo, gql } from 'apollo-angular';
|
||||||
import { QueryResult } from 'src/app/model/entities/query-result';
|
import { QueryResult } from 'src/app/model/entities/query-result';
|
||||||
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
|
import {
|
||||||
|
DB_HISTORY_MODEL_FRAGMENT,
|
||||||
|
DB_MODEL_FRAGMENT,
|
||||||
|
} from 'src/app/model/graphql/db-model.query';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { SpinnerService } from 'src/app/service/spinner.service';
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RolesDataService
|
export class RolesDataService
|
||||||
extends PageDataService<Role>
|
extends PageWithHistoryDataService<Role>
|
||||||
implements
|
implements
|
||||||
Create<Role, RoleCreateInput>,
|
Create<Role, RoleCreateInput>,
|
||||||
Update<Role, RoleUpdateInput>,
|
Update<Role, RoleUpdateInput>,
|
||||||
@ -59,6 +64,10 @@ export class RolesDataService
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
|
||||||
...DB_MODEL
|
...DB_MODEL
|
||||||
}
|
}
|
||||||
@ -99,6 +108,10 @@ export class RolesDataService
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
username
|
||||||
|
}
|
||||||
|
|
||||||
...DB_MODEL
|
...DB_MODEL
|
||||||
}
|
}
|
||||||
@ -120,6 +133,44 @@ export class RolesDataService
|
|||||||
.pipe(map(result => result.data.roles.nodes[0]));
|
.pipe(map(result => result.data.roles.nodes[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadHistory(id: number): Observable<DbModel[]> {
|
||||||
|
return this.apollo
|
||||||
|
.query<{ roles: QueryResult<Role> }>({
|
||||||
|
query: gql`
|
||||||
|
query getRoleHistory($id: Int) {
|
||||||
|
roles(filter: { id: { equal: $id } }) {
|
||||||
|
count
|
||||||
|
totalCount
|
||||||
|
nodes {
|
||||||
|
history {
|
||||||
|
id
|
||||||
|
|
||||||
|
name
|
||||||
|
description
|
||||||
|
permissions {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
...DB_HISTORY_MODEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${DB_HISTORY_MODEL_FRAGMENT}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError(err => {
|
||||||
|
this.spinner.hide();
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(map(result => result.data?.roles?.nodes?.[0]?.history ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
onChange(): Observable<void> {
|
onChange(): Observable<void> {
|
||||||
return this.apollo
|
return this.apollo
|
||||||
.subscribe<{ roleChange: void }>({
|
.subscribe<{ roleChange: void }>({
|
||||||
@ -213,7 +264,7 @@ export class RolesDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ role: { delete: boolean } }>({
|
.mutate<{ role: { delete: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation deleteRole($id: ID!) {
|
mutation deleteRole($id: Int!) {
|
||||||
role {
|
role {
|
||||||
delete(id: $id)
|
delete(id: $id)
|
||||||
}
|
}
|
||||||
@ -236,7 +287,7 @@ export class RolesDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ role: { restore: boolean } }>({
|
.mutate<{ role: { restore: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation restoreRole($id: ID!) {
|
mutation restoreRole($id: Int!) {
|
||||||
role {
|
role {
|
||||||
restore(id: $id)
|
restore(id: $id)
|
||||||
}
|
}
|
||||||
@ -260,7 +311,7 @@ export class RolesDataService
|
|||||||
.query<{ permissions: QueryResult<Permission> }>({
|
.query<{ permissions: QueryResult<Permission> }>({
|
||||||
query: gql`
|
query: gql`
|
||||||
query getPermissions {
|
query getPermissions {
|
||||||
permissions {
|
permissions(sort: [{ name: ASC }]) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -284,6 +335,10 @@ export class RolesDataService
|
|||||||
provide: PageDataService,
|
provide: PageDataService,
|
||||||
useClass: RolesDataService,
|
useClass: RolesDataService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PageWithHistoryDataService,
|
||||||
|
useClass: RolesDataService,
|
||||||
|
},
|
||||||
RolesDataService,
|
RolesDataService,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { RoleFormPageComponent } from 'src/app/modules/admin/administration/role
|
|||||||
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page';
|
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page';
|
||||||
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
|
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service';
|
||||||
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns';
|
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns';
|
||||||
|
import { HistoryComponent } from 'src/app/modules/admin/administration/roles/history/history.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -31,12 +32,20 @@ const routes: Routes = [
|
|||||||
permissions: [PermissionsEnum.rolesUpdate],
|
permissions: [PermissionsEnum.rolesUpdate],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'history/:historyId',
|
||||||
|
component: HistoryComponent,
|
||||||
|
canActivate: [PermissionGuard],
|
||||||
|
data: {
|
||||||
|
permissions: [PermissionsEnum.roles],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RolesPage, RoleFormPageComponent],
|
declarations: [RolesPage, RoleFormPageComponent, HistoryComponent],
|
||||||
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
||||||
providers: [RolesDataService.provide(), RolesColumns.provide()],
|
providers: [RolesDataService.provide(), RolesColumns.provide()],
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
[(skip)]="skip"
|
[(skip)]="skip"
|
||||||
[(take)]="take"
|
[(take)]="take"
|
||||||
(load)="load()"
|
(load)="load()"
|
||||||
|
[history]="true"
|
||||||
[create]="true"
|
[create]="true"
|
||||||
[update]="true"
|
[update]="true"
|
||||||
(delete)="delete($event)"
|
(delete)="delete($event)"
|
||||||
|
@ -5,6 +5,8 @@ import { SpinnerService } from 'src/app/service/spinner.service';
|
|||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@ -24,10 +26,18 @@ export class SettingsPage implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
private spinner: SpinnerService,
|
private spinner: SpinnerService,
|
||||||
private data: SettingsDataService,
|
private data: SettingsDataService,
|
||||||
private fb: FormBuilder
|
private fb: FormBuilder,
|
||||||
|
private auth: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const isAllowed = await this.auth.hasAnyPermissionLazy([
|
||||||
|
PermissionsEnum.settings,
|
||||||
|
]);
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
this.data
|
this.data
|
||||||
.onUpdate()
|
.onUpdate()
|
||||||
.pipe(takeUntil(this.unsubscribe$))
|
.pipe(takeUntil(this.unsubscribe$))
|
||||||
@ -66,7 +76,14 @@ export class SettingsPage implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
async save() {
|
||||||
|
const isAllowed = await this.auth.hasAnyPermissionLazy([
|
||||||
|
PermissionsEnum.settingsUpdate,
|
||||||
|
]);
|
||||||
|
if (!isAllowed) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.settingsForm.pristine) {
|
if (this.settingsForm.pristine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template formPageContent>
|
<ng-template formPageContent>
|
||||||
<ng-container *ngIf="notExistingUsers; else showId"></ng-container>
|
<ng-container *ngIf="!this.nodeId; else showId">
|
||||||
<div class="form-page-input">
|
<div class="form-page-input">
|
||||||
<p class="label">{{ 'common.id' | translate }}</p>
|
<p class="label">{{ 'common.id' | translate }}</p>
|
||||||
<p-dropdown
|
<p-dropdown
|
||||||
@ -24,9 +24,12 @@
|
|||||||
optionValue="keycloakId"
|
optionValue="keycloakId"
|
||||||
formControlName="keycloakId"></p-dropdown>
|
formControlName="keycloakId"></p-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
<ng-template #showId>
|
<ng-template #showId>
|
||||||
|
<div class="form-page-input">
|
||||||
<p class="label">{{ 'common.id' | translate }}</p>
|
<p class="label">{{ 'common.id' | translate }}</p>
|
||||||
<input pInputText class="value" type="number" formControlName="id" />
|
<input pInputText class="value" type="number" formControlName="id" />
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="form-page-input">
|
<div class="form-page-input">
|
||||||
|
@ -33,7 +33,7 @@ export class UserFormPageComponent extends FormPageBase<
|
|||||||
|
|
||||||
if (!this.nodeId) {
|
if (!this.nodeId) {
|
||||||
this.dataService.getNotExistingUsersFromKeycloak().subscribe(users => {
|
this.dataService.getNotExistingUsersFromKeycloak().subscribe(users => {
|
||||||
this.notExistingUsers = users;
|
this.notExistingUsers = users ?? [];
|
||||||
this.node = this.new();
|
this.node = this.new();
|
||||||
this.setForm(this.node);
|
this.setForm(this.node);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
<app-history-sidebar
|
||||||
|
[loadHistory]="loadHistory"
|
||||||
|
[columns]="columns"></app-history-sidebar>
|
@ -0,0 +1,65 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryComponent } from './history.component';
|
||||||
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { PageDataService } from 'src/app/core/base/page.data.service';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { MockPageColumns } from 'src/app/modules/shared/test/page.columns.mock';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
|
||||||
|
|
||||||
|
describe('HistoryComponent', () => {
|
||||||
|
let component: HistoryComponent<unknown>;
|
||||||
|
let fixture: ComponentFixture<HistoryComponent<unknown>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [HistoryComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
KeycloakService,
|
||||||
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: { historyId: '3' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageDataService,
|
||||||
|
useClass: UsersDataService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PageColumns,
|
||||||
|
useClass: MockPageColumns,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
component.loadHistory = (id: number) => of([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
import { PageColumns } from 'src/app/core/base/page.columns';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history',
|
||||||
|
templateUrl: './history.component.html',
|
||||||
|
styleUrl: './history.component.scss',
|
||||||
|
})
|
||||||
|
export class HistoryComponent<T> {
|
||||||
|
loadHistory = (id: number) => this.data.loadHistory(id);
|
||||||
|
columns = this._columns.get();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public data: PageWithHistoryDataService<T>,
|
||||||
|
private _columns: PageColumns<T>
|
||||||
|
) {}
|
||||||
|
}
|
@ -12,6 +12,14 @@ export class UsersColumns extends PageColumns<User> {
|
|||||||
get(): TableColumn<User>[] {
|
get(): TableColumn<User>[] {
|
||||||
return [
|
return [
|
||||||
ID_COLUMN,
|
ID_COLUMN,
|
||||||
|
{
|
||||||
|
name: 'keycloakId',
|
||||||
|
translationKey: 'user.keycloak_id',
|
||||||
|
type: 'text',
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
value: (row: User) => row.keycloakId,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'username',
|
name: 'username',
|
||||||
translationKey: 'user.username',
|
translationKey: 'user.username',
|
||||||
@ -28,8 +36,10 @@ export class UsersColumns extends PageColumns<User> {
|
|||||||
name: 'roles',
|
name: 'roles',
|
||||||
translationKey: 'common.roles',
|
translationKey: 'common.roles',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
filterable: true,
|
filterable: false,
|
||||||
value: (row: User) => row.roles.map(role => role.name).join(', '),
|
sortable: false,
|
||||||
|
value: (row: User) =>
|
||||||
|
(row?.roles ?? []).map(role => role.name).join(', '),
|
||||||
width: '300px',
|
width: '300px',
|
||||||
},
|
},
|
||||||
...DB_MODEL_COLUMNS,
|
...DB_MODEL_COLUMNS,
|
||||||
|
@ -11,7 +11,10 @@ import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
|||||||
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
import { Sort } from 'src/app/model/graphql/filter/sort.model';
|
||||||
import { Apollo, gql } from 'apollo-angular';
|
import { Apollo, gql } from 'apollo-angular';
|
||||||
import { QueryResult } from 'src/app/model/entities/query-result';
|
import { QueryResult } from 'src/app/model/entities/query-result';
|
||||||
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query';
|
import {
|
||||||
|
DB_HISTORY_MODEL_FRAGMENT,
|
||||||
|
DB_MODEL_FRAGMENT,
|
||||||
|
} from 'src/app/model/graphql/db-model.query';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { SpinnerService } from 'src/app/service/spinner.service';
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
import {
|
import {
|
||||||
@ -21,10 +24,12 @@ import {
|
|||||||
UserUpdateInput,
|
UserUpdateInput,
|
||||||
} from 'src/app/model/auth/user';
|
} from 'src/app/model/auth/user';
|
||||||
import { Role } from 'src/app/model/entities/role';
|
import { Role } from 'src/app/model/entities/role';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
import { PageWithHistoryDataService } from 'src/app/core/base/page-with-history.data.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersDataService
|
export class UsersDataService
|
||||||
extends PageDataService<User>
|
extends PageWithHistoryDataService<User>
|
||||||
implements
|
implements
|
||||||
Create<User, UserCreateInput>,
|
Create<User, UserCreateInput>,
|
||||||
Update<User, UserUpdateInput>,
|
Update<User, UserUpdateInput>,
|
||||||
@ -61,6 +66,7 @@ export class UsersDataService
|
|||||||
keycloakId
|
keycloakId
|
||||||
username
|
username
|
||||||
email
|
email
|
||||||
|
|
||||||
roles {
|
roles {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@ -127,6 +133,44 @@ export class UsersDataService
|
|||||||
.pipe(map(result => result.data.users.nodes[0]));
|
.pipe(map(result => result.data.users.nodes[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadHistory(id: number): Observable<DbModel[]> {
|
||||||
|
return this.apollo
|
||||||
|
.query<{ users: QueryResult<User> }>({
|
||||||
|
query: gql`
|
||||||
|
query getUserHistory($id: Int) {
|
||||||
|
users(filter: { id: { equal: $id } }) {
|
||||||
|
count
|
||||||
|
totalCount
|
||||||
|
nodes {
|
||||||
|
history {
|
||||||
|
id
|
||||||
|
keycloakId
|
||||||
|
username
|
||||||
|
email
|
||||||
|
roles {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
...DB_HISTORY_MODEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${DB_HISTORY_MODEL_FRAGMENT}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError(err => {
|
||||||
|
this.spinner.hide();
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(map(result => result.data?.users?.nodes?.[0]?.history ?? []));
|
||||||
|
}
|
||||||
|
|
||||||
onChange(): Observable<void> {
|
onChange(): Observable<void> {
|
||||||
return this.apollo
|
return this.apollo
|
||||||
.subscribe<{ userChange: void }>({
|
.subscribe<{ userChange: void }>({
|
||||||
@ -213,7 +257,7 @@ export class UsersDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ user: { delete: boolean } }>({
|
.mutate<{ user: { delete: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation deleteUser($id: ID!) {
|
mutation deleteUser($id: Int!) {
|
||||||
user {
|
user {
|
||||||
delete(id: $id)
|
delete(id: $id)
|
||||||
}
|
}
|
||||||
@ -236,7 +280,7 @@ export class UsersDataService
|
|||||||
return this.apollo
|
return this.apollo
|
||||||
.mutate<{ user: { restore: boolean } }>({
|
.mutate<{ user: { restore: boolean } }>({
|
||||||
mutation: gql`
|
mutation: gql`
|
||||||
mutation restoreUser($id: ID!) {
|
mutation restoreUser($id: Int!) {
|
||||||
user {
|
user {
|
||||||
restore(id: $id)
|
restore(id: $id)
|
||||||
}
|
}
|
||||||
@ -280,16 +324,14 @@ export class UsersDataService
|
|||||||
|
|
||||||
getNotExistingUsersFromKeycloak(): Observable<NotExistingUser[]> {
|
getNotExistingUsersFromKeycloak(): Observable<NotExistingUser[]> {
|
||||||
return this.apollo
|
return this.apollo
|
||||||
.query<{ notExistingUsersFromKeycloak: QueryResult<NotExistingUser> }>({
|
.query<{ notExistingUsersFromKeycloak: NotExistingUser[] }>({
|
||||||
query: gql`
|
query: gql`
|
||||||
query getNotExistingUsers {
|
query getNotExistingUsers {
|
||||||
notExistingUsersFromKeycloak {
|
notExistingUsersFromKeycloak {
|
||||||
nodes {
|
|
||||||
keycloakId
|
keycloakId
|
||||||
username
|
username
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -298,7 +340,7 @@ export class UsersDataService
|
|||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(map(result => result.data.notExistingUsersFromKeycloak.nodes));
|
.pipe(map(result => result.data.notExistingUsersFromKeycloak));
|
||||||
}
|
}
|
||||||
|
|
||||||
static provide(): Provider[] {
|
static provide(): Provider[] {
|
||||||
@ -307,6 +349,10 @@ export class UsersDataService
|
|||||||
provide: PageDataService,
|
provide: PageDataService,
|
||||||
useClass: UsersDataService,
|
useClass: UsersDataService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PageWithHistoryDataService,
|
||||||
|
useClass: UsersDataService,
|
||||||
|
},
|
||||||
UsersDataService,
|
UsersDataService,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { UserFormPageComponent } from 'src/app/modules/admin/administration/user
|
|||||||
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page';
|
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page';
|
||||||
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
|
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service';
|
||||||
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns';
|
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns';
|
||||||
|
import { HistoryComponent } from 'src/app/modules/admin/administration/users/history/history.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -31,12 +32,20 @@ const routes: Routes = [
|
|||||||
permissions: [PermissionsEnum.usersUpdate],
|
permissions: [PermissionsEnum.usersUpdate],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'history/:historyId',
|
||||||
|
component: HistoryComponent,
|
||||||
|
canActivate: [PermissionGuard],
|
||||||
|
data: {
|
||||||
|
permissions: [PermissionsEnum.users],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [UsersPage, UserFormPageComponent],
|
declarations: [UsersPage, UserFormPageComponent, HistoryComponent],
|
||||||
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
imports: [CommonModule, SharedModule, RouterModule.forChild(routes)],
|
||||||
providers: [UsersDataService.provide(), UsersColumns.provide()],
|
providers: [UsersDataService.provide(), UsersColumns.provide()],
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
[(skip)]="skip"
|
[(skip)]="skip"
|
||||||
[(take)]="take"
|
[(take)]="take"
|
||||||
(load)="load()"
|
(load)="load()"
|
||||||
|
[history]="true"
|
||||||
[create]="true"
|
[create]="true"
|
||||||
[update]="true"
|
[update]="true"
|
||||||
(delete)="delete($event)"
|
(delete)="delete($event)"
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<p-sidebar
|
||||||
|
[(visible)]="showSidebar"
|
||||||
|
position="right"
|
||||||
|
[baseZIndex]="10000"
|
||||||
|
styleClass="w-1/3"
|
||||||
|
(onHide)="close()">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<h1 class="text-xl font-bold">
|
||||||
|
{{ 'history.header' | translate }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div class="h-full">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div *ngFor="let entry of history" class="p-4 rounded-lg bg2">
|
||||||
|
<div
|
||||||
|
*ngFor="let item of entry | keyvalue"
|
||||||
|
class="grid grid-cols-6 gap-4 items-center mb-2">
|
||||||
|
<div class="font-medium col-span-2">
|
||||||
|
{{ getAttributeTranslationKey(item.key) | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 text-center">-></div>
|
||||||
|
<div class="value col-span-3 overflow-hidden">
|
||||||
|
{{ item.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p-sidebar>
|
@ -0,0 +1,54 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistorySidebarComponent } from 'src/app/modules/shared/components/history/history-sidebar.component';
|
||||||
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
describe('HistorySidebarComponent', () => {
|
||||||
|
let component: HistorySidebarComponent<unknown>;
|
||||||
|
let fixture: ComponentFixture<HistorySidebarComponent<unknown>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [HistorySidebarComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserAnimationsModule,
|
||||||
|
SharedModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
KeycloakService,
|
||||||
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: { historyId: '3' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HistorySidebarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.columns = [];
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
component.loadHistory = (id: number) => of([]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,138 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
|
import { User } from 'src/app/model/auth/user';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TableColumn } from 'src/app/modules/shared/components/table/table.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
interface History {
|
||||||
|
id?: number;
|
||||||
|
editor?: User;
|
||||||
|
deleted?: boolean;
|
||||||
|
created?: Date;
|
||||||
|
updated?: Date;
|
||||||
|
|
||||||
|
[x: string | number | symbol]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history-sidebar',
|
||||||
|
templateUrl: './history-sidebar.component.html',
|
||||||
|
styleUrl: './history-sidebar.component.scss',
|
||||||
|
})
|
||||||
|
export class HistorySidebarComponent<T> implements OnInit {
|
||||||
|
@Input({ required: true }) loadHistory!: (
|
||||||
|
id: number
|
||||||
|
) => Observable<DbModel[]>;
|
||||||
|
@Input({ required: true }) columns!: TableColumn<T>[];
|
||||||
|
historyId!: number;
|
||||||
|
|
||||||
|
private translations: { [key: string]: string } = {};
|
||||||
|
public history: History[] = [];
|
||||||
|
public showSidebar: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private spinner: SpinnerService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.spinner.show();
|
||||||
|
const id = this.route.snapshot.params['historyId'];
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('History ID is required');
|
||||||
|
}
|
||||||
|
this.historyId = +id;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
open(): void {
|
||||||
|
this.columns.forEach(column => {
|
||||||
|
this.translations[column.name] = column.translationKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showSidebar = true;
|
||||||
|
this.loadHistory(this.historyId).subscribe(data => {
|
||||||
|
this.history = data ? this.processHistory(data) : [];
|
||||||
|
this.spinner.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
let oldHistory: Partial<History> = {};
|
||||||
|
for (const history of this.history) {
|
||||||
|
const attributes = Object.keys(history).map(key => {
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (history[attribute] === oldHistory[attribute]) {
|
||||||
|
delete oldHistory[attribute];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldHistory = history;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
const backRoute = this.historyId ? '../..' : '..';
|
||||||
|
|
||||||
|
this.router.navigate([backRoute], { relativeTo: this.route }).then(() => {
|
||||||
|
this.spinner.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processHistory(data: DbModel[]): History[] {
|
||||||
|
const history = (data as History[]).map(entry => {
|
||||||
|
const filteredEntry: History = {};
|
||||||
|
for (const key in entry) {
|
||||||
|
if (!key.startsWith('_') && !key.startsWith('__')) {
|
||||||
|
filteredEntry[key] = this.flattenValue(entry[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredEntry;
|
||||||
|
});
|
||||||
|
|
||||||
|
const reversedHistory = [...history].reverse();
|
||||||
|
|
||||||
|
let cumulativeHistory: Partial<History> = {};
|
||||||
|
for (const entry of reversedHistory) {
|
||||||
|
const attributes = Object.keys(entry);
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (
|
||||||
|
attribute !== 'editor' &&
|
||||||
|
entry[attribute] === cumulativeHistory[attribute]
|
||||||
|
) {
|
||||||
|
delete entry[attribute];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cumulativeHistory = { ...cumulativeHistory, ...entry };
|
||||||
|
}
|
||||||
|
|
||||||
|
return reversedHistory.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private flattenValue(value: unknown): string {
|
||||||
|
if (value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return (
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Object.entries(value)
|
||||||
|
.filter(([key]) => !key.startsWith('_') && !key.startsWith('__'))
|
||||||
|
.map(([, val]) => val)
|
||||||
|
)
|
||||||
|
.map(item => this.flattenValue(item))
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAttributeTranslationKey(key: string): string {
|
||||||
|
return this.translations[key] || `history.${key}`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,38 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { MenuBarComponent } from "src/app/modules/shared/components/menu-bar/menu-bar.component";
|
import { MenuBarComponent } from 'src/app/modules/shared/components/menu-bar/menu-bar.component';
|
||||||
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
describe("MenuBarComponent", () => {
|
describe('MenuBarComponent', () => {
|
||||||
let component: MenuBarComponent;
|
let component: MenuBarComponent;
|
||||||
let fixture: ComponentFixture<MenuBarComponent>;
|
let fixture: ComponentFixture<MenuBarComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [MenuBarComponent],
|
declarations: [MenuBarComponent],
|
||||||
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
KeycloakService,
|
||||||
|
ErrorHandlingService,
|
||||||
|
ToastService,
|
||||||
|
MessageService,
|
||||||
|
ConfirmationService,
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: { params: of({}) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(MenuBarComponent);
|
fixture = TestBed.createComponent(MenuBarComponent);
|
||||||
@ -16,7 +40,7 @@ describe("MenuBarComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { SideMenuComponent } from "./side-menu.component";
|
import { SideMenuComponent } from './side-menu.component';
|
||||||
|
|
||||||
describe("SideMenuComponent", () => {
|
describe('SideMenuComponent', () => {
|
||||||
let component: SideMenuComponent;
|
let component: SideMenuComponent;
|
||||||
let fixture: ComponentFixture<SideMenuComponent>;
|
let fixture: ComponentFixture<SideMenuComponent>;
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ describe("SideMenuComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input } from '@angular/core';
|
||||||
import { MenuElement } from "src/app/model/view/menu-element";
|
import { MenuElement } from 'src/app/model/view/menu-element';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-side-menu",
|
selector: 'app-side-menu',
|
||||||
templateUrl: "./side-menu.component.html",
|
templateUrl: './side-menu.component.html',
|
||||||
styleUrl: "./side-menu.component.scss",
|
styleUrl: './side-menu.component.scss',
|
||||||
})
|
})
|
||||||
export class SideMenuComponent {
|
export class SideMenuComponent {
|
||||||
@Input() elements: MenuElement[] = [];
|
@Input() elements: MenuElement[] = [];
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<p-sidebar
|
<p-sidebar
|
||||||
[visible]="true"
|
[visible]="true"
|
||||||
position="right"
|
position="right"
|
||||||
[style]="{ width: 'min-content', minWidth: '450px' }"
|
[style]="{ width: 'min-content', minWidth: '500px' }"
|
||||||
(onHide)="onClose.emit()"
|
(onHide)="onClose.emit()"
|
||||||
(visibleChange)="!$event ? onClose.emit() : undefined">
|
(visibleChange)="!$event ? onClose.emit() : undefined">
|
||||||
<ng-template pTemplate="headless">
|
<ng-template pTemplate="headless">
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { FormPageComponent } from "src/app/modules/shared/components/slidein/form-page.component";
|
import { FormPageComponent } from 'src/app/modules/shared/components/slidein/form-page.component';
|
||||||
import { SharedModule } from "src/app/modules/shared/shared.module";
|
import { SharedModule } from 'src/app/modules/shared/shared.module';
|
||||||
import { TranslateModule } from "@ngx-translate/core";
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { AuthService } from "src/app/service/auth.service";
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
import { KeycloakService } from "keycloak-angular";
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ErrorHandlingService } from "src/app/service/error-handling.service";
|
import { ErrorHandlingService } from 'src/app/service/error-handling.service';
|
||||||
import { ToastService } from "src/app/service/toast.service";
|
import { ToastService } from 'src/app/service/toast.service';
|
||||||
import { ConfirmationService, MessageService } from "primeng/api";
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { of } from "rxjs";
|
import { of } from 'rxjs';
|
||||||
import { FormGroup } from "@angular/forms";
|
import { FormGroup } from '@angular/forms';
|
||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
describe("FormpageComponent", () => {
|
describe('FormpageComponent', () => {
|
||||||
let component: FormPageComponent<string>;
|
let component: FormPageComponent<string>;
|
||||||
let fixture: ComponentFixture<FormPageComponent<string>>;
|
let fixture: ComponentFixture<FormPageComponent<string>>;
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ describe("FormpageComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create", () => {
|
it('should create', () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,18 +5,18 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
} from "@angular/core";
|
} from '@angular/core';
|
||||||
import { FormGroup } from "@angular/forms";
|
import { FormGroup } from '@angular/forms';
|
||||||
import { SpinnerService } from "src/app/service/spinner.service";
|
import { SpinnerService } from 'src/app/service/spinner.service';
|
||||||
import {
|
import {
|
||||||
FormPageContentDirective,
|
FormPageContentDirective,
|
||||||
FormPageHeaderDirective,
|
FormPageHeaderDirective,
|
||||||
} from "src/app/modules/shared/form";
|
} from 'src/app/modules/shared/form';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-form-page",
|
selector: 'app-form-page',
|
||||||
templateUrl: "./form-page.component.html",
|
templateUrl: './form-page.component.html',
|
||||||
styleUrl: "./form-page.component.scss",
|
styleUrl: './form-page.component.scss',
|
||||||
})
|
})
|
||||||
export class FormPageComponent<T> {
|
export class FormPageComponent<T> {
|
||||||
@Input({ required: true }) formGroup!: FormGroup;
|
@Input({ required: true }) formGroup!: FormGroup;
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
*ngFor="let column of visibleColumns"
|
*ngFor="let column of visibleColumns"
|
||||||
[pSortableColumn]="column.name"
|
[pSortableColumn]="column.sortable != false ? column.name : undefined"
|
||||||
[class]="column.class ?? ''"
|
[class]="column.class ?? ''"
|
||||||
[style.min-width]="
|
[style.min-width]="
|
||||||
column.minWidth ? column.minWidth + ' !important' : ''
|
column.minWidth ? column.minWidth + ' !important' : ''
|
||||||
@ -85,7 +85,9 @@
|
|||||||
">
|
">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span>{{ column.translationKey | translate }}</span>
|
<span>{{ column.translationKey | translate }}</span>
|
||||||
<p-sortIcon [field]="column.name"></p-sortIcon>
|
<p-sortIcon
|
||||||
|
[field]="column.name"
|
||||||
|
*ngIf="column.sortable != false"></p-sortIcon>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th *ngIf="update || delete.observed || customRowActionsVisible"></th>
|
<th *ngIf="update || delete.observed || customRowActionsVisible"></th>
|
||||||
@ -217,6 +219,13 @@
|
|||||||
pTooltip="{{ 'table.restore' | translate }}"
|
pTooltip="{{ 'table.restore' | translate }}"
|
||||||
(click)="restore.emit(row)"
|
(click)="restore.emit(row)"
|
||||||
[disabled]="rowDisabled(row)"></p-button>
|
[disabled]="rowDisabled(row)"></p-button>
|
||||||
|
<p-button
|
||||||
|
*ngIf="history"
|
||||||
|
class="icon-btn btn"
|
||||||
|
icon="pi pi-history"
|
||||||
|
tooltipPosition="left"
|
||||||
|
pTooltip="{{ 'table.history' | translate }}"
|
||||||
|
routerLink="history/{{ row.id }}"></p-button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -18,7 +18,7 @@ import { RowsPerPageOption } from 'src/app/service/filter.service';
|
|||||||
import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
||||||
import { Sort, SortOrder } from 'src/app/model/graphql/filter/sort.model';
|
import { Sort, SortOrder } from 'src/app/model/graphql/filter/sort.model';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { debounceTime, Subject } from 'rxjs';
|
import { debounceTime, Observable, Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { Logger } from 'src/app/service/logger.service';
|
import { Logger } from 'src/app/service/logger.service';
|
||||||
import { AuthService } from 'src/app/service/auth.service';
|
import { AuthService } from 'src/app/service/auth.service';
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
CustomActionsDirective,
|
CustomActionsDirective,
|
||||||
CustomRowActionsDirective,
|
CustomRowActionsDirective,
|
||||||
} from 'src/app/modules/shared/components/table/table';
|
} from 'src/app/modules/shared/components/table/table';
|
||||||
|
import { DbModel } from 'src/app/model/entities/db-model';
|
||||||
|
|
||||||
const logger = new Logger('TableComponent');
|
const logger = new Logger('TableComponent');
|
||||||
|
|
||||||
@ -76,12 +77,14 @@ export class TableComponent<T> implements OnInit {
|
|||||||
|
|
||||||
// eslint-disable-next-line @angular-eslint/no-output-native
|
// eslint-disable-next-line @angular-eslint/no-output-native
|
||||||
@Output() load = new EventEmitter<void>();
|
@Output() load = new EventEmitter<void>();
|
||||||
|
@Input() loadHistory?: (id: number) => Observable<DbModel[] | undefined>;
|
||||||
@Input() loading = true;
|
@Input() loading = true;
|
||||||
|
|
||||||
@Input() dataKey = 'id';
|
@Input() dataKey = 'id';
|
||||||
@Input() responsiveLayout: 'stack' | 'scroll' = 'stack';
|
@Input() responsiveLayout: 'stack' | 'scroll' = 'stack';
|
||||||
@Input() selectableColumns = true;
|
@Input() selectableColumns = true;
|
||||||
|
|
||||||
|
@Input() history = false;
|
||||||
@Input() create = false;
|
@Input() create = false;
|
||||||
@Input() update = false;
|
@Input() update = false;
|
||||||
@Input() createBaseUrl = '';
|
@Input() createBaseUrl = '';
|
||||||
@ -157,13 +160,6 @@ export class TableComponent<T> implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolveColumns() {
|
resolveColumns() {
|
||||||
if (!this.rows || this.rows.length == 0) {
|
|
||||||
this.resolvedColumns = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedColumns: ResolvedTableColumn<T>[][] = [];
|
|
||||||
|
|
||||||
const columns = this.columns
|
const columns = this.columns
|
||||||
.map(x => {
|
.map(x => {
|
||||||
if (x.visible === undefined || x.visible === null) {
|
if (x.visible === undefined || x.visible === null) {
|
||||||
@ -173,6 +169,12 @@ export class TableComponent<T> implements OnInit {
|
|||||||
})
|
})
|
||||||
.filter(x => !x.hidden && x.visible === true);
|
.filter(x => !x.hidden && x.visible === true);
|
||||||
|
|
||||||
|
if (!this.rows || this.rows.length == 0) {
|
||||||
|
this.resolvedColumns = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedColumns: ResolvedTableColumn<T>[][] = [];
|
||||||
this.rows.forEach(row => {
|
this.rows.forEach(row => {
|
||||||
const resolvedRow: ResolvedTableColumn<T>[] = [];
|
const resolvedRow: ResolvedTableColumn<T>[] = [];
|
||||||
columns.forEach(column => {
|
columns.forEach(column => {
|
||||||
@ -297,9 +299,24 @@ export class TableComponent<T> implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFilterForm() {
|
setFilterForm() {
|
||||||
this.filterForm = this.defaultFilterForm;
|
this.filterForm = this.defaultFilterForm;
|
||||||
|
|
||||||
|
this.filter.forEach(f => {
|
||||||
|
Object.keys(f).forEach(key => {
|
||||||
|
const value = f[key];
|
||||||
|
if (this.filterForm.contains(key)) {
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
Object.keys(value).forEach(subKey => {
|
||||||
|
this.filterForm.get(key)?.setValue(value[subKey]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.filterForm.get(key)?.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.filterForm.valueChanges
|
this.filterForm.valueChanges
|
||||||
.pipe(takeUntil(this.unsubscriber$), debounceTime(200))
|
.pipe(takeUntil(this.unsubscriber$), debounceTime(200))
|
||||||
.subscribe(changes => {
|
.subscribe(changes => {
|
||||||
@ -345,6 +362,10 @@ export class TableComponent<T> implements OnInit {
|
|||||||
|
|
||||||
const filterMode = column.filterMode || defaultFilterMode;
|
const filterMode = column.filterMode || defaultFilterMode;
|
||||||
|
|
||||||
|
if (column.filterSelector) {
|
||||||
|
return column.filterSelector(filterMode, value);
|
||||||
|
}
|
||||||
|
|
||||||
return { [key]: { [filterMode]: value } };
|
return { [key]: { [filterMode]: value } };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
|
||||||
|
import { Filter } from 'src/app/model/graphql/filter/filter.model';
|
||||||
|
|
||||||
export type TableColumnValue<T> = T | T[keyof T] | string;
|
export type TableColumnValue<T> = T | T[keyof T] | string;
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ export interface TableColumn<T> {
|
|||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
filterMode?: 'contains' | 'startsWith' | 'endsWith' | 'equals';
|
filterMode?: 'contains' | 'startsWith' | 'endsWith' | 'equals';
|
||||||
sort?: (a: T, b: T) => number;
|
sort?: (a: T, b: T) => number;
|
||||||
filter?: (row: T, filter: string) => boolean;
|
filterSelector?: (mode: string, value: unknown) => Filter;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { addMinutes, format, parseISO } from "date-fns";
|
import { addMinutes, format, parseISO } from 'date-fns';
|
||||||
|
|
||||||
export function formatUTCDateToClientTimezone(date: string) {
|
export function formatUTCDateToClientTimezone(date: string) {
|
||||||
const dateTZ = addMinutes(
|
const dateTZ = addMinutes(
|
||||||
parseISO(date),
|
parseISO(date),
|
||||||
new Date().getTimezoneOffset() * -1,
|
new Date().getTimezoneOffset() * -1
|
||||||
);
|
);
|
||||||
return format(dateTZ, "yyyy-MM-dd HH:mm:ss");
|
return format(dateTZ, 'yyyy-MM-dd HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatUTCDateToGermanLocaleAndTimezone(date: string) {
|
export function formatUTCDateToGermanLocaleAndTimezone(date: string) {
|
||||||
const dateTZ = addMinutes(
|
const dateTZ = addMinutes(
|
||||||
parseISO(date),
|
parseISO(date),
|
||||||
new Date().getTimezoneOffset() * -1,
|
new Date().getTimezoneOffset() * -1
|
||||||
);
|
);
|
||||||
return format(dateTZ, "dd.MM.yy HH:mm:ss");
|
return format(dateTZ, 'dd.MM.yy HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,31 @@
|
|||||||
export function deepCopy<T>(obj: T): T {
|
export function simpleDeepCopy<T>(obj: T): T {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deepCopy<T>(obj: T): T {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
const arrCopy = [] as unknown[];
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
arrCopy[i] = deepCopy(obj[i]);
|
||||||
|
}
|
||||||
|
return arrCopy as unknown as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Function) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objCopy = {} as { [key: string]: unknown };
|
||||||
|
for (const key in obj) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
objCopy[key] = deepCopy((obj as { [key: string]: unknown })[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return objCopy as T;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Directive, TemplateRef } from "@angular/core";
|
import { Directive, TemplateRef } from '@angular/core';
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||||
selector: "[formPageHeader]",
|
selector: '[formPageHeader]',
|
||||||
})
|
})
|
||||||
export class FormPageHeaderDirective {
|
export class FormPageHeaderDirective {
|
||||||
constructor(public templateRef: TemplateRef<unknown>) {}
|
constructor(public templateRef: TemplateRef<unknown>) {}
|
||||||
@ -10,7 +10,7 @@ export class FormPageHeaderDirective {
|
|||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||||
selector: "[formPageContent]",
|
selector: '[formPageContent]',
|
||||||
})
|
})
|
||||||
export class FormPageContentDirective {
|
export class FormPageContentDirective {
|
||||||
constructor(public templateRef: TemplateRef<unknown>) {}
|
constructor(public templateRef: TemplateRef<unknown>) {}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user