From 1001b6db5f1602f640823de92fbfbedc341f5593 Mon Sep 17 00:00:00 2001 From: edraft Date: Sat, 11 Jan 2025 00:41:29 +0100 Subject: [PATCH] [WIP] Added domain management --- api/src/api_graphql/filter/domain_filter.py | 13 + api/src/api_graphql/graphql/domain.gql | 53 ++++ api/src/api_graphql/graphql/mutation.gql | 1 + api/src/api_graphql/graphql/query.gql | 1 + api/src/api_graphql/graphql/short_url.gql | 3 + .../api_graphql/input/domain_create_input.py | 13 + .../api_graphql/input/domain_update_input.py | 18 ++ .../input/short_url_create_input.py | 5 + .../input/short_url_update_input.py | 5 + api/src/api_graphql/mutation.py | 10 + .../api_graphql/mutations/domain_mutation.py | 75 ++++++ .../mutations/short_url_mutation.py | 12 + api/src/api_graphql/queries/domain_query.py | 17 ++ .../api_graphql/queries/short_url_query.py | 1 + api/src/api_graphql/query.py | 17 ++ api/src/data/schemas/public/domain.py | 27 ++ api/src/data/schemas/public/domain_dao.py | 22 ++ api/src/data/schemas/public/short_url.py | 19 ++ api/src/data/schemas/public/short_url_dao.py | 1 + .../data/scripts/2025-01-10-23-15-domains.sql | 32 +++ .../service/permission/permissions_enum.py | 6 + web/src/app/model/auth/permissionsEnum.ts | 45 ++-- web/src/app/model/entities/domain.ts | 14 ++ web/src/app/model/entities/short-url.ts | 4 + web/src/app/modules/admin/admin.module.ts | 9 + .../modules/admin/domains/domains.columns.ts | 35 +++ .../admin/domains/domains.data.service.ts | 232 ++++++++++++++++++ .../modules/admin/domains/domains.module.ts | 43 ++++ .../modules/admin/domains/domains.page.html | 19 ++ .../modules/admin/domains/domains.page.scss | 0 .../admin/domains/domains.page.spec.ts | 51 ++++ .../app/modules/admin/domains/domains.page.ts | 72 ++++++ .../form-page/domain-form-page.component.html | 31 +++ .../form-page/domain-form-page.component.scss | 0 .../domain-form-page.component.spec.ts | 50 ++++ .../form-page/domain-form-page.component.ts | 93 +++++++ .../short-url-form-page.component.html | 17 +- .../short-url-form-page.component.ts | 13 + .../short-urls/short-urls.data.service.ts | 34 +++ .../admin/short-urls/short-urls.page.html | 4 + web/src/app/service/sidebar.service.ts | 8 + web/src/assets/i18n/de.json | 5 + web/src/assets/i18n/en.json | 5 + web/src/styles/theme.scss | 9 +- web/src/styles/theme_maxlan.scss | 9 +- 45 files changed, 1130 insertions(+), 23 deletions(-) create mode 100644 api/src/api_graphql/filter/domain_filter.py create mode 100644 api/src/api_graphql/graphql/domain.gql create mode 100644 api/src/api_graphql/input/domain_create_input.py create mode 100644 api/src/api_graphql/input/domain_update_input.py create mode 100644 api/src/api_graphql/mutations/domain_mutation.py create mode 100644 api/src/api_graphql/queries/domain_query.py create mode 100644 api/src/data/schemas/public/domain.py create mode 100644 api/src/data/schemas/public/domain_dao.py create mode 100644 api/src/data/scripts/2025-01-10-23-15-domains.sql create mode 100644 web/src/app/model/entities/domain.ts create mode 100644 web/src/app/modules/admin/domains/domains.columns.ts create mode 100644 web/src/app/modules/admin/domains/domains.data.service.ts create mode 100644 web/src/app/modules/admin/domains/domains.module.ts create mode 100644 web/src/app/modules/admin/domains/domains.page.html create mode 100644 web/src/app/modules/admin/domains/domains.page.scss create mode 100644 web/src/app/modules/admin/domains/domains.page.spec.ts create mode 100644 web/src/app/modules/admin/domains/domains.page.ts create mode 100644 web/src/app/modules/admin/domains/form-page/domain-form-page.component.html create mode 100644 web/src/app/modules/admin/domains/form-page/domain-form-page.component.scss create mode 100644 web/src/app/modules/admin/domains/form-page/domain-form-page.component.spec.ts create mode 100644 web/src/app/modules/admin/domains/form-page/domain-form-page.component.ts diff --git a/api/src/api_graphql/filter/domain_filter.py b/api/src/api_graphql/filter/domain_filter.py new file mode 100644 index 0000000..ba1240b --- /dev/null +++ b/api/src/api_graphql/filter/domain_filter.py @@ -0,0 +1,13 @@ +from api_graphql.abc.db_model_filter_abc import DbModelFilterABC +from api_graphql.abc.filter.string_filter import StringFilter + + +class DomainFilter(DbModelFilterABC): + def __init__( + self, + obj: dict, + ): + DbModelFilterABC.__init__(self, obj) + + self.add_field("name", StringFilter) + self.add_field("description", StringFilter) diff --git a/api/src/api_graphql/graphql/domain.gql b/api/src/api_graphql/graphql/domain.gql new file mode 100644 index 0000000..ab59428 --- /dev/null +++ b/api/src/api_graphql/graphql/domain.gql @@ -0,0 +1,53 @@ +type DomainResult { + totalCount: Int + count: Int + nodes: [Domain] +} + +type Domain implements DbModel { + id: ID + name: String + + shortUrls: [ShortUrl] + + deleted: Boolean + editor: User + createdUtc: String + updatedUtc: String +} + +input DomainSort { + id: SortOrder + name: SortOrder + + deleted: SortOrder + editorId: SortOrder + createdUtc: SortOrder + updatedUtc: SortOrder +} + +input DomainFilter { + id: IntFilter + name: StringFilter + + deleted: BooleanFilter + editor: IntFilter + createdUtc: DateFilter + updatedUtc: DateFilter +} + +type DomainMutation { + create(input: DomainCreateInput!): Domain + update(input: DomainUpdateInput!): Domain + delete(id: ID!): Boolean + restore(id: ID!): Boolean +} + +input DomainCreateInput { + name: String! +} + +input DomainUpdateInput { + id: ID! + name: String +} \ No newline at end of file diff --git a/api/src/api_graphql/graphql/mutation.gql b/api/src/api_graphql/graphql/mutation.gql index 3b93c89..cee4ca0 100644 --- a/api/src/api_graphql/graphql/mutation.gql +++ b/api/src/api_graphql/graphql/mutation.gql @@ -5,5 +5,6 @@ type Mutation { role: RoleMutation group: GroupMutation + domain: DomainMutation shortUrl: ShortUrlMutation } \ No newline at end of file diff --git a/api/src/api_graphql/graphql/query.gql b/api/src/api_graphql/graphql/query.gql index 5e086db..5d0e151 100644 --- a/api/src/api_graphql/graphql/query.gql +++ b/api/src/api_graphql/graphql/query.gql @@ -11,6 +11,7 @@ type Query { userHasAnyPermission(permissions: [String]!): Boolean notExistingUsersFromKeycloak: KeycloakUserResult + domains(filter: [DomainFilter], sort: [DomainSort], skip: Int, take: Int): DomainResult groups(filter: [GroupFilter], sort: [GroupSort], skip: Int, take: Int): GroupResult shortUrls(filter: [ShortUrlFilter], sort: [ShortUrlSort], skip: Int, take: Int): ShortUrlResult } \ No newline at end of file diff --git a/api/src/api_graphql/graphql/short_url.gql b/api/src/api_graphql/graphql/short_url.gql index ce8f9d8..ec5f1b4 100644 --- a/api/src/api_graphql/graphql/short_url.gql +++ b/api/src/api_graphql/graphql/short_url.gql @@ -11,6 +11,7 @@ type ShortUrl implements DbModel { description: String visits: Int group: Group + domain: Domain loadingScreen: Boolean deleted: Boolean @@ -55,6 +56,7 @@ input ShortUrlCreateInput { targetUrl: String! description: String groupId: ID + domainId: ID loadingScreen: Boolean } @@ -64,5 +66,6 @@ input ShortUrlUpdateInput { targetUrl: String description: String groupId: ID + domainId: ID loadingScreen: Boolean } diff --git a/api/src/api_graphql/input/domain_create_input.py b/api/src/api_graphql/input/domain_create_input.py new file mode 100644 index 0000000..e9aef8e --- /dev/null +++ b/api/src/api_graphql/input/domain_create_input.py @@ -0,0 +1,13 @@ +from api_graphql.abc.input_abc import InputABC + + +class DomainCreateInput(InputABC): + + def __init__(self, src: dict): + InputABC.__init__(self, src) + + self._name = self.option("name", str, required=True) + + @property + def name(self) -> str: + return self._name diff --git a/api/src/api_graphql/input/domain_update_input.py b/api/src/api_graphql/input/domain_update_input.py new file mode 100644 index 0000000..71f5684 --- /dev/null +++ b/api/src/api_graphql/input/domain_update_input.py @@ -0,0 +1,18 @@ +from api_graphql.abc.input_abc import InputABC + + +class DomainUpdateInput(InputABC): + + def __init__(self, src: dict): + InputABC.__init__(self, src) + + self._id = self.option("id", int, required=True) + self._name = self.option("name", str) + + @property + def id(self) -> int: + return self._id + + @property + def name(self) -> str: + return self._name diff --git a/api/src/api_graphql/input/short_url_create_input.py b/api/src/api_graphql/input/short_url_create_input.py index bb01aca..94c4b9e 100644 --- a/api/src/api_graphql/input/short_url_create_input.py +++ b/api/src/api_graphql/input/short_url_create_input.py @@ -12,6 +12,7 @@ class ShortUrlCreateInput(InputABC): self._target_url = self.option("targetUrl", str, required=True) self._description = self.option("description", str) self._group_id = self.option("groupId", int) + self._domain_id = self.option("domainId", int) self._loading_screen = self.option("loadingScreen", bool) @property @@ -30,6 +31,10 @@ class ShortUrlCreateInput(InputABC): def group_id(self) -> Optional[int]: return self._group_id + @property + def domain_id(self) -> Optional[int]: + return self._domain_id + @property def loading_screen(self) -> Optional[str]: return self._loading_screen diff --git a/api/src/api_graphql/input/short_url_update_input.py b/api/src/api_graphql/input/short_url_update_input.py index 2e4bb13..059098c 100644 --- a/api/src/api_graphql/input/short_url_update_input.py +++ b/api/src/api_graphql/input/short_url_update_input.py @@ -13,6 +13,7 @@ class ShortUrlUpdateInput(InputABC): self._target_url = self.option("targetUrl", str) self._description = self.option("description", str) self._group_id = self.option("groupId", int) + self._domain_id = self.option("domainId", int) self._loading_screen = self.option("loadingScreen", bool) @property @@ -35,6 +36,10 @@ class ShortUrlUpdateInput(InputABC): def group_id(self) -> Optional[int]: return self._group_id + @property + def domain_id(self) -> Optional[int]: + return self._domain_id + @property def loading_screen(self) -> Optional[str]: return self._loading_screen diff --git a/api/src/api_graphql/mutation.py b/api/src/api_graphql/mutation.py index c745ca5..26df1b1 100644 --- a/api/src/api_graphql/mutation.py +++ b/api/src/api_graphql/mutation.py @@ -33,6 +33,16 @@ class Mutation(MutationABC): ], ) + + self.add_mutation_type( + "domain", + "Domain", + require_any_permission=[ + Permissions.domains_create, + Permissions.domains_update, + Permissions.domains_delete, + ], + ) self.add_mutation_type( "group", "Group", diff --git a/api/src/api_graphql/mutations/domain_mutation.py b/api/src/api_graphql/mutations/domain_mutation.py new file mode 100644 index 0000000..339704b --- /dev/null +++ b/api/src/api_graphql/mutations/domain_mutation.py @@ -0,0 +1,75 @@ +from api_graphql.abc.mutation_abc import MutationABC +from api_graphql.input.domain_create_input import DomainCreateInput +from api_graphql.input.domain_update_input import DomainUpdateInput +from api_graphql.input.group_create_input import GroupCreateInput +from api_graphql.input.group_update_input import GroupUpdateInput +from core.logger import APILogger +from data.schemas.public.domain_dao import domainDao +from data.schemas.public.group import Group +from service.permission.permissions_enum import Permissions + +logger = APILogger(__name__) + + +class DomainMutation(MutationABC): + def __init__(self): + MutationABC.__init__(self, "Domain") + + self.mutation( + "create", + self.resolve_create, + DomainCreateInput, + require_any_permission=[Permissions.domains_create], + ) + self.mutation( + "update", + self.resolve_update, + DomainUpdateInput, + require_any_permission=[Permissions.domains_update], + ) + self.mutation( + "delete", + self.resolve_delete, + require_any_permission=[Permissions.domains_delete], + ) + self.mutation( + "restore", + self.resolve_restore, + require_any_permission=[Permissions.domains_delete], + ) + + @staticmethod + async def resolve_create(obj: GroupCreateInput, *_): + logger.debug(f"create domain: {obj.__dict__}") + + domain = Group( + 0, + obj.name, + ) + nid = await domainDao.create(domain) + return await domainDao.get_by_id(nid) + + @staticmethod + async def resolve_update(obj: GroupUpdateInput, *_): + logger.debug(f"update domain: {input}") + + if obj.name is not None: + domain = await domainDao.get_by_id(obj.id) + domain.name = obj.name + await domainDao.update(domain) + + return await domainDao.get_by_id(obj.id) + + @staticmethod + async def resolve_delete(*_, id: str): + logger.debug(f"delete domain: {id}") + domain = await domainDao.get_by_id(id) + await domainDao.delete(domain) + return True + + @staticmethod + async def resolve_restore(*_, id: str): + logger.debug(f"restore domain: {id}") + domain = await domainDao.get_by_id(id) + await domainDao.restore(domain) + return True diff --git a/api/src/api_graphql/mutations/short_url_mutation.py b/api/src/api_graphql/mutations/short_url_mutation.py index cc67d8d..6705994 100644 --- a/api/src/api_graphql/mutations/short_url_mutation.py +++ b/api/src/api_graphql/mutations/short_url_mutation.py @@ -4,6 +4,7 @@ from api_graphql.abc.mutation_abc import MutationABC from api_graphql.input.short_url_create_input import ShortUrlCreateInput from api_graphql.input.short_url_update_input import ShortUrlUpdateInput from core.logger import APILogger +from data.schemas.public.domain_dao import domainDao from data.schemas.public.group_dao import groupDao from data.schemas.public.short_url import ShortUrl from data.schemas.public.short_url_dao import shortUrlDao @@ -49,6 +50,7 @@ class ShortUrlMutation(MutationABC): obj.target_url, obj.description, obj.group_id, + obj.domain_id, obj.loading_screen, ) nid = await shortUrlDao.create(short_url) @@ -74,6 +76,16 @@ class ShortUrlMutation(MutationABC): if group_by_id is None: raise NotFound(f"Group with id {obj.group_id} does not exist") short_url.group_id = obj.group_id + else: + short_url.group_id = None + + if obj.domain_id is not None: + domain_by_id = await domainDao.find_by_id(obj.domain_id) + if domain_by_id is None: + raise NotFound(f"Domain with id {obj.domain_id} does not exist") + short_url.domain_id = obj.domain_id + else: + short_url.domain_id = None if obj.loading_screen is not None: short_url.loading_screen = obj.loading_screen diff --git a/api/src/api_graphql/queries/domain_query.py b/api/src/api_graphql/queries/domain_query.py new file mode 100644 index 0000000..e3bfc43 --- /dev/null +++ b/api/src/api_graphql/queries/domain_query.py @@ -0,0 +1,17 @@ +from api_graphql.abc.db_model_query_abc import DbModelQueryABC +from data.schemas.public.domain import Domain +from data.schemas.public.group import Group +from data.schemas.public.short_url import ShortUrl +from data.schemas.public.short_url_dao import shortUrlDao + + +class DomainQuery(DbModelQueryABC): + def __init__(self): + DbModelQueryABC.__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}) diff --git a/api/src/api_graphql/queries/short_url_query.py b/api/src/api_graphql/queries/short_url_query.py index 29c750a..10561b2 100644 --- a/api/src/api_graphql/queries/short_url_query.py +++ b/api/src/api_graphql/queries/short_url_query.py @@ -9,5 +9,6 @@ class ShortUrlQuery(DbModelQueryABC): 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) diff --git a/api/src/api_graphql/query.py b/api/src/api_graphql/query.py index d80bacb..43eeceb 100644 --- a/api/src/api_graphql/query.py +++ b/api/src/api_graphql/query.py @@ -5,6 +5,7 @@ from api_graphql.abc.sort_abc import Sort from api_graphql.field.dao_field_builder import DaoFieldBuilder from api_graphql.field.resolver_field_builder import ResolverFieldBuilder from api_graphql.filter.api_key_filter import ApiKeyFilter +from api_graphql.filter.domain_filter import DomainFilter from api_graphql.filter.group_filter import GroupFilter from api_graphql.filter.permission_filter import PermissionFilter from api_graphql.filter.role_filter import RoleFilter @@ -18,6 +19,8 @@ from data.schemas.permission.permission import Permission from data.schemas.permission.permission_dao import permissionDao from data.schemas.permission.role import Role from data.schemas.permission.role_dao import roleDao +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_dao import groupDao from data.schemas.public.short_url import ShortUrl @@ -80,6 +83,20 @@ class Query(QueryABC): .with_require_any_permission([Permissions.users_create]) ) + + self.field( + DaoFieldBuilder("domains") + .with_dao(domainDao) + .with_filter(DomainFilter) + .with_sort(Sort[Domain]) + .with_require_any_permission( + [ + Permissions.domains, + Permissions.short_urls_create, + Permissions.short_urls_update, + ] + ) + ) self.field( DaoFieldBuilder("groups") .with_dao(groupDao) diff --git a/api/src/data/schemas/public/domain.py b/api/src/data/schemas/public/domain.py new file mode 100644 index 0000000..0f0ec30 --- /dev/null +++ b/api/src/data/schemas/public/domain.py @@ -0,0 +1,27 @@ +from datetime import datetime +from typing import Optional + +from core.database.abc.db_model_abc import DbModelABC +from core.typing import SerialId + + +class Domain(DbModelABC): + def __init__( + self, + id: SerialId, + name: str, + deleted: bool = False, + editor_id: Optional[SerialId] = None, + created: Optional[datetime] = None, + updated: Optional[datetime] = None, + ): + DbModelABC.__init__(self, id, deleted, editor_id, created, updated) + self._name = name + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = value diff --git a/api/src/data/schemas/public/domain_dao.py b/api/src/data/schemas/public/domain_dao.py new file mode 100644 index 0000000..dd5317d --- /dev/null +++ b/api/src/data/schemas/public/domain_dao.py @@ -0,0 +1,22 @@ +from core.logger import DBLogger +from data.schemas.public.domain import Domain +from data.schemas.public.group import Group + +logger = DBLogger(__name__) + +from core.database.abc.db_model_dao_abc import DbModelDaoABC + + +class DomainDao(DbModelDaoABC[Group]): + def __init__(self): + DbModelDaoABC.__init__(self, __name__, Group, "public.domains") + self.attribute(Domain.name, str) + + async def get_by_name(self, name: str) -> Group: + result = await self._db.select_map( + f"SELECT * FROM {self._table_name} WHERE Name = '{name}'" + ) + return self.to_object(result[0]) + + +domainDao = DomainDao() diff --git a/api/src/data/schemas/public/short_url.py b/api/src/data/schemas/public/short_url.py index 3c699bf..c9ba579 100644 --- a/api/src/data/schemas/public/short_url.py +++ b/api/src/data/schemas/public/short_url.py @@ -16,6 +16,7 @@ class ShortUrl(DbModelABC): target_url: str, description: Optional[str], group_id: Optional[SerialId], + domain_id: Optional[SerialId], loading_screen: Optional[str] = None, deleted: bool = False, editor_id: Optional[SerialId] = None, @@ -27,6 +28,7 @@ class ShortUrl(DbModelABC): self._target_url = target_url self._description = description self._group_id = group_id + self._domain_id = domain_id self._loading_screen = loading_screen @property @@ -70,6 +72,23 @@ class ShortUrl(DbModelABC): return await groupDao.get_by_id(self._group_id) + @property + def domain_id(self) -> SerialId: + return self._domain_id + + @domain_id.setter + def domain_id(self, value: SerialId): + self._domain_id = value + + @async_property + async def domain(self) -> Optional[Group]: + if self._domain_id is None: + return None + + from data.schemas.public.domain_dao import domainDao + + return await domainDao.get_by_id(self._domain_id) + @async_property async def visit_count(self) -> int: from data.schemas.public.short_url_visit_dao import shortUrlVisitDao diff --git a/api/src/data/schemas/public/short_url_dao.py b/api/src/data/schemas/public/short_url_dao.py index 90165ac..43c5a26 100644 --- a/api/src/data/schemas/public/short_url_dao.py +++ b/api/src/data/schemas/public/short_url_dao.py @@ -13,6 +13,7 @@ class ShortUrlDao(DbModelDaoABC[ShortUrl]): self.attribute(ShortUrl.target_url, str) self.attribute(ShortUrl.description, str) self.attribute(ShortUrl.group_id, int) + self.attribute(ShortUrl.domain_id, int) self.attribute(ShortUrl.loading_screen, bool) diff --git a/api/src/data/scripts/2025-01-10-23-15-domains.sql b/api/src/data/scripts/2025-01-10-23-15-domains.sql new file mode 100644 index 0000000..e1a95c4 --- /dev/null +++ b/api/src/data/scripts/2025-01-10-23-15-domains.sql @@ -0,0 +1,32 @@ +CREATE + SCHEMA IF NOT EXISTS public; + +-- groups +CREATE TABLE IF NOT EXISTS public.domains +( + Id SERIAL PRIMARY KEY, + Name VARCHAR(255) NOT NULL, + -- for history + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + EditorId INT NULL REFERENCES administration.users (Id), + CreatedUtc timestamptz NOT NULL DEFAULT NOW(), + UpdatedUtc timestamptz NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS public.domains_history +( + LIKE public.domains +); + +CREATE TRIGGER domains_history_trigger + BEFORE INSERT OR UPDATE OR DELETE + ON public.domains + FOR EACH ROW +EXECUTE FUNCTION public.history_trigger_function(); + +ALTER TABLE public.short_urls + ADD COLUMN domainId INT NULL REFERENCES public.domains (Id); + +ALTER TABLE public.short_urls_history + ADD COLUMN domainId INT NULL REFERENCES public.domains (Id); + diff --git a/api/src/service/permission/permissions_enum.py b/api/src/service/permission/permissions_enum.py index fedfc2d..1fd1abc 100644 --- a/api/src/service/permission/permissions_enum.py +++ b/api/src/service/permission/permissions_enum.py @@ -31,6 +31,12 @@ class Permissions(Enum): """ Public """ + # domains + domains = "domains" + domains_create = "domains.create" + domains_update = "domains.update" + domains_delete = "domains.delete" + # groups groups = "groups" groups_create = "groups.create" diff --git a/web/src/app/model/auth/permissionsEnum.ts b/web/src/app/model/auth/permissionsEnum.ts index 184b4f2..b063db6 100644 --- a/web/src/app/model/auth/permissionsEnum.ts +++ b/web/src/app/model/auth/permissionsEnum.ts @@ -1,30 +1,35 @@ export enum PermissionsEnum { // Administration - apiKeys = "api_keys", - apiKeysCreate = "api_keys.create", - apiKeysUpdate = "api_keys.update", - apiKeysDelete = "api_keys.delete", + apiKeys = 'api_keys', + apiKeysCreate = 'api_keys.create', + apiKeysUpdate = 'api_keys.update', + apiKeysDelete = 'api_keys.delete', // Users - users = "users", - usersCreate = "users.create", - usersUpdate = "users.update", - usersDelete = "users.delete", + users = 'users', + usersCreate = 'users.create', + usersUpdate = 'users.update', + usersDelete = 'users.delete', // Permissions - roles = "roles", - rolesCreate = "roles.create", - rolesUpdate = "roles.update", - rolesDelete = "roles.delete", + roles = 'roles', + rolesCreate = 'roles.create', + rolesUpdate = 'roles.update', + rolesDelete = 'roles.delete', // Public - groups = "groups", - groupsCreate = "groups.create", - groupsUpdate = "groups.update", - groupsDelete = "groups.delete", + domains = 'domains', + domainsCreate = 'domains.create', + domainsUpdate = 'domains.update', + domainsDelete = 'domains.delete', - shortUrls = "short_urls", - shortUrlsCreate = "short_urls.create", - shortUrlsUpdate = "short_urls.update", - shortUrlsDelete = "short_urls.delete", + groups = 'groups', + groupsCreate = 'groups.create', + groupsUpdate = 'groups.update', + groupsDelete = 'groups.delete', + + shortUrls = 'short_urls', + shortUrlsCreate = 'short_urls.create', + shortUrlsUpdate = 'short_urls.update', + shortUrlsDelete = 'short_urls.delete', } diff --git a/web/src/app/model/entities/domain.ts b/web/src/app/model/entities/domain.ts new file mode 100644 index 0000000..c81d199 --- /dev/null +++ b/web/src/app/model/entities/domain.ts @@ -0,0 +1,14 @@ +import { DbModel } from 'src/app/model/entities/db-model'; + +export interface Domain extends DbModel { + name: string; +} + +export interface DomainCreateInput { + name: string; +} + +export interface DomainUpdateInput { + id: number; + name: string; +} diff --git a/web/src/app/model/entities/short-url.ts b/web/src/app/model/entities/short-url.ts index 3007473..8ddaba4 100644 --- a/web/src/app/model/entities/short-url.ts +++ b/web/src/app/model/entities/short-url.ts @@ -1,5 +1,6 @@ import { DbModel } from 'src/app/model/entities/db-model'; import { Group } from 'src/app/model/entities/group'; +import { Domain } from 'src/app/model/entities/domain'; export interface ShortUrl extends DbModel { shortUrl: string; @@ -8,6 +9,7 @@ export interface ShortUrl extends DbModel { loadingScreen: boolean; visits: number; group?: Group; + domain?: Domain; } export interface ShortUrlDto { @@ -22,6 +24,7 @@ export interface ShortUrlCreateInput { description: string; loadingScreen: boolean; groupId: number; + domainId: number; } export interface ShortUrlUpdateInput { @@ -31,4 +34,5 @@ export interface ShortUrlUpdateInput { description: string; loadingScreen: boolean; groupId: number; + domainId: number; } diff --git a/web/src/app/modules/admin/admin.module.ts b/web/src/app/modules/admin/admin.module.ts index 15f924e..57af25b 100644 --- a/web/src/app/modules/admin/admin.module.ts +++ b/web/src/app/modules/admin/admin.module.ts @@ -6,6 +6,15 @@ import { PermissionGuard } from 'src/app/core/guard/permission.guard'; import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; const routes: Routes = [ + { + path: 'domains', + loadChildren: () => + import('src/app/modules/admin/domains/domains.module').then( + m => m.DomainsModule + ), + canActivate: [PermissionGuard], + data: { permissions: [PermissionsEnum.domains] }, + }, { path: 'groups', loadChildren: () => diff --git a/web/src/app/modules/admin/domains/domains.columns.ts b/web/src/app/modules/admin/domains/domains.columns.ts new file mode 100644 index 0000000..3138c19 --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.columns.ts @@ -0,0 +1,35 @@ +import { Injectable, Provider } from '@angular/core'; +import { + DB_MODEL_COLUMNS, + ID_COLUMN, + PageColumns, +} from 'src/app/core/base/page.columns'; +import { TableColumn } from 'src/app/modules/shared/components/table/table.model'; +import { Domain } from 'src/app/model/entities/domain'; + +@Injectable() +export class DomainsColumns extends PageColumns { + get(): TableColumn[] { + return [ + ID_COLUMN, + { + name: 'name', + label: 'common.name', + type: 'text', + filterable: true, + value: (row: Domain) => row.name, + }, + ...DB_MODEL_COLUMNS, + ]; + } + + static provide(): Provider[] { + return [ + { + provide: PageColumns, + useClass: DomainsColumns, + }, + DomainsColumns, + ]; + } +} diff --git a/web/src/app/modules/admin/domains/domains.data.service.ts b/web/src/app/modules/admin/domains/domains.data.service.ts new file mode 100644 index 0000000..133790f --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.data.service.ts @@ -0,0 +1,232 @@ +import { Injectable, Provider } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + Create, + Delete, + PageDataService, + Restore, + Update, +} from 'src/app/core/base/page.data.service'; +import { Filter } from 'src/app/model/graphql/filter/filter.model'; +import { Sort } from 'src/app/model/graphql/filter/sort.model'; +import { Apollo, gql } from 'apollo-angular'; +import { QueryResult } from 'src/app/model/entities/query-result'; +import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query'; +import { catchError, map } from 'rxjs/operators'; +import { SpinnerService } from 'src/app/service/spinner.service'; +import { + Domain, + DomainCreateInput, + DomainUpdateInput, +} from 'src/app/model/entities/domain'; + +@Injectable() +export class DomainsDataService + extends PageDataService + implements + Create, + Update, + Delete, + Restore +{ + constructor( + private spinner: SpinnerService, + private apollo: Apollo + ) { + super(); + } + + load( + filter?: Filter[] | undefined, + sort?: Sort[] | undefined, + skip?: number | undefined, + take?: number | undefined + ): Observable> { + return this.apollo + .query<{ domains: QueryResult }>({ + query: gql` + query getDomains( + $filter: [DomainFilter] + $sort: [DomainSort] + $skip: Int + $take: Int + ) { + domains(filter: $filter, sort: $sort, skip: $skip, take: $take) { + count + totalCount + nodes { + id + name + + ...DB_MODEL + } + } + } + + ${DB_MODEL_FRAGMENT} + `, + variables: { + filter: filter, + sort: sort, + skip: skip, + take: take, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data.domains)); + } + + loadById(id: number): Observable { + return this.apollo + .query<{ domains: QueryResult }>({ + query: gql` + query getDomain($id: Int) { + domain(filter: { id: { equal: $id } }) { + id + name + + ...DB_MODEL + } + } + + ${DB_MODEL_FRAGMENT} + `, + variables: { + id: id, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data.domains.nodes[0])); + } + + create(object: DomainCreateInput): Observable { + return this.apollo + .mutate<{ domain: { create: Domain } }>({ + mutation: gql` + mutation createDomain($input: DomainCreateInput!) { + domain { + create(input: $input) { + id + name + + ...DB_MODEL + } + } + } + + ${DB_MODEL_FRAGMENT} + `, + variables: { + input: { + name: object.name, + }, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data?.domain.create)); + } + + update(object: DomainUpdateInput): Observable { + return this.apollo + .mutate<{ domain: { update: Domain } }>({ + mutation: gql` + mutation updateDomain($input: DomainUpdateInput!) { + domain { + update(input: $input) { + id + name + + ...DB_MODEL + } + } + } + + ${DB_MODEL_FRAGMENT} + `, + variables: { + input: { + id: object.id, + name: object.name, + }, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data?.domain.update)); + } + + delete(object: Domain): Observable { + return this.apollo + .mutate<{ domain: { delete: boolean } }>({ + mutation: gql` + mutation deleteDomain($id: ID!) { + domain { + delete(id: $id) + } + } + `, + variables: { + id: object.id, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data?.domain.delete ?? false)); + } + + restore(object: Domain): Observable { + return this.apollo + .mutate<{ domain: { restore: boolean } }>({ + mutation: gql` + mutation restoreDomain($id: ID!) { + domain { + restore(id: $id) + } + } + `, + variables: { + id: object.id, + }, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data?.domain.restore ?? false)); + } + + static provide(): Provider[] { + return [ + { + provide: PageDataService, + useClass: DomainsDataService, + }, + DomainsDataService, + ]; + } +} diff --git a/web/src/app/modules/admin/domains/domains.module.ts b/web/src/app/modules/admin/domains/domains.module.ts new file mode 100644 index 0000000..62fe081 --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from 'src/app/modules/shared/shared.module'; +import { RouterModule, Routes } from '@angular/router'; +import { PermissionGuard } from 'src/app/core/guard/permission.guard'; +import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; +import { DomainsPage } from 'src/app/modules/admin/domains/domains.page'; +import { DomainFormPageComponent } from 'src/app/modules/admin/domains/form-page/domain-form-page.component'; +import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service'; +import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns'; + +const routes: Routes = [ + { + path: '', + title: 'Domains', + component: DomainsPage, + children: [ + { + path: 'create', + component: DomainFormPageComponent, + canActivate: [PermissionGuard], + data: { + permissions: [PermissionsEnum.apiKeysCreate], + }, + }, + { + path: 'edit/:id', + component: DomainFormPageComponent, + canActivate: [PermissionGuard], + data: { + permissions: [PermissionsEnum.apiKeysUpdate], + }, + }, + ], + }, +]; + +@NgModule({ + declarations: [DomainsPage, DomainFormPageComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], + providers: [DomainsDataService.provide(), DomainsColumns.provide()], +}) +export class DomainsModule {} diff --git a/web/src/app/modules/admin/domains/domains.page.html b/web/src/app/modules/admin/domains/domains.page.html new file mode 100644 index 0000000..dd8b0a9 --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.page.html @@ -0,0 +1,19 @@ + + + diff --git a/web/src/app/modules/admin/domains/domains.page.scss b/web/src/app/modules/admin/domains/domains.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/src/app/modules/admin/domains/domains.page.spec.ts b/web/src/app/modules/admin/domains/domains.page.spec.ts new file mode 100644 index 0000000..f1ba746 --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.page.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page"; +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 { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service"; + +describe("ApiKeysComponent", () => { + let component: ApiKeysPage; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApiKeysPage], + imports: [SharedModule, TranslateModule.forRoot()], + providers: [ + AuthService, + KeycloakService, + ErrorHandlingService, + ToastService, + MessageService, + ConfirmationService, + { + provide: ActivatedRoute, + useValue: { + snapshot: { params: of({}) }, + }, + }, + { + provide: PageDataService, + useClass: ApiKeysDataService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ApiKeysPage); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/src/app/modules/admin/domains/domains.page.ts b/web/src/app/modules/admin/domains/domains.page.ts new file mode 100644 index 0000000..bc10a7f --- /dev/null +++ b/web/src/app/modules/admin/domains/domains.page.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { PageBase } from 'src/app/core/base/page-base'; +import { ToastService } from 'src/app/service/toast.service'; +import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; +import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; +import { Group } from 'src/app/model/entities/group'; +import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service'; +import { DomainsColumns } from 'src/app/modules/admin/domains/domains.columns'; + +@Component({ + selector: 'app-domains', + templateUrl: './domains.page.html', + styleUrl: './domains.page.scss', +}) +export class DomainsPage extends PageBase< + Group, + DomainsDataService, + DomainsColumns +> { + constructor( + private toast: ToastService, + private confirmation: ConfirmationDialogService + ) { + super(true, { + read: [PermissionsEnum.domains], + create: [PermissionsEnum.domainsCreate], + update: [PermissionsEnum.domainsUpdate], + delete: [PermissionsEnum.domainsDelete], + restore: [PermissionsEnum.domainsDelete], + }); + } + + load(): void { + this.loading = true; + this.dataService + .load(this.filter, this.sort, this.skip, this.take) + .subscribe(result => { + this.result = result; + this.loading = false; + }); + } + + delete(group: Group): void { + this.confirmation.confirmDialog({ + header: 'dialog.delete.header', + message: 'dialog.delete.message', + accept: () => { + this.loading = true; + this.dataService.delete(group).subscribe(() => { + this.toast.success('action.deleted'); + this.load(); + }); + }, + messageParams: { entity: group.name }, + }); + } + + restore(group: Group): void { + this.confirmation.confirmDialog({ + header: 'dialog.restore.header', + message: 'dialog.restore.message', + accept: () => { + this.loading = true; + this.dataService.restore(group).subscribe(() => { + this.toast.success('action.restored'); + this.load(); + }); + }, + messageParams: { entity: group.name }, + }); + } +} diff --git a/web/src/app/modules/admin/domains/form-page/domain-form-page.component.html b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.html new file mode 100644 index 0000000..82d8e33 --- /dev/null +++ b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.html @@ -0,0 +1,31 @@ + + +

+ {{ 'common.group' | translate }} + {{ + (isUpdate ? 'sidebar.header.update' : 'sidebar.header.create') + | translate + }} +

+
+ + +
+

{{ 'common.id' | translate }}

+ +
+
+

{{ 'common.name' | translate }}

+ +
+
+
diff --git a/web/src/app/modules/admin/domains/form-page/domain-form-page.component.scss b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/src/app/modules/admin/domains/form-page/domain-form-page.component.spec.ts b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.spec.ts new file mode 100644 index 0000000..d7bfe12 --- /dev/null +++ b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.spec.ts @@ -0,0 +1,50 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.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 { 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 { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; + +describe("ApiKeyFormpageComponent", () => { + let component: RoleFormPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RoleFormPageComponent], + imports: [ + BrowserAnimationsModule, + SharedModule, + TranslateModule.forRoot(), + ], + providers: [ + AuthService, + ErrorHandlingService, + ToastService, + MessageService, + ConfirmationService, + { + provide: ActivatedRoute, + useValue: { + snapshot: { params: of({}) }, + }, + }, + ApiKeysDataService, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(RoleFormPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/web/src/app/modules/admin/domains/form-page/domain-form-page.component.ts b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.ts new file mode 100644 index 0000000..f5994bb --- /dev/null +++ b/web/src/app/modules/admin/domains/form-page/domain-form-page.component.ts @@ -0,0 +1,93 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ToastService } from 'src/app/service/toast.service'; +import { FormPageBase } from 'src/app/core/base/form-page-base'; +import { + Domain, + DomainCreateInput, + DomainUpdateInput, +} from 'src/app/model/entities/domain'; +import { DomainsDataService } from 'src/app/modules/admin/domains/domains.data.service'; + +@Component({ + selector: 'app-domain-form-page', + templateUrl: './domain-form-page.component.html', + styleUrl: './domain-form-page.component.scss', +}) +export class DomainFormPageComponent extends FormPageBase< + Domain, + DomainCreateInput, + DomainUpdateInput, + DomainsDataService +> { + constructor(private toast: ToastService) { + super(); + if (!this.nodeId) { + this.node = this.new(); + this.setForm(this.node); + + return; + } + + this.dataService + .load([{ id: { equal: this.nodeId } }]) + .subscribe(apiKey => { + this.node = apiKey.nodes[0]; + this.setForm(this.node); + }); + } + + new(): Domain { + return {} as Domain; + } + + buildForm() { + this.form = new FormGroup({ + id: new FormControl(undefined), + name: new FormControl(undefined, Validators.required), + }); + this.form.controls['id'].disable(); + } + + setForm(node?: Domain) { + this.form.controls['id'].setValue(node?.id); + this.form.controls['name'].setValue(node?.name); + } + + getCreateInput(): DomainCreateInput { + return { + name: this.form.controls['name'].pristine + ? undefined + : (this.form.controls['name'].value ?? undefined), + }; + } + + getUpdateInput(): DomainUpdateInput { + if (!this.node?.id) { + throw new Error('Node id is missing'); + } + + return { + id: this.form.controls['id'].value, + name: this.form.controls['name'].pristine + ? undefined + : (this.form.controls['name'].value ?? undefined), + }; + } + + create(apiKey: DomainCreateInput): void { + this.dataService.create(apiKey).subscribe(() => { + this.spinner.hide(); + this.toast.success('action.created'); + this.close(); + }); + } + + update(apiKey: DomainUpdateInput): void { + this.dataService.update(apiKey).subscribe(() => { + this.spinner.hide(); + this.toast.success('action.created'); + this.close(); + }); + } +} diff --git a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html index a078c82..8240320 100644 --- a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html +++ b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.html @@ -6,7 +6,7 @@ (onClose)="close()">

- {{ 'common.group' | translate }} + {{ 'common.short_url' | translate }} {{ (isUpdate ? 'sidebar.header.update' : 'sidebar.header.create') | translate @@ -65,5 +65,20 @@ > +
+

{{ 'common.domain' | translate }}

+
+ +
+
diff --git a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts index e8849f8..1143817 100644 --- a/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts +++ b/web/src/app/modules/admin/short-urls/form-page/short-url-form-page.component.ts @@ -9,6 +9,7 @@ import { } from 'src/app/model/entities/short-url'; import { ShortUrlsDataService } from 'src/app/modules/admin/short-urls/short-urls.data.service'; import { Group } from 'src/app/model/entities/group'; +import { Domain } from 'src/app/model/entities/domain'; @Component({ selector: 'app-short-url-form-page', @@ -22,12 +23,16 @@ export class ShortUrlFormPageComponent extends FormPageBase< ShortUrlsDataService > { groups: Group[] = []; + domains: Domain[] = []; constructor(private toast: ToastService) { super(); this.dataService.getAllGroups().subscribe(groups => { this.groups = groups; }); + this.dataService.getAllDomains().subscribe(domains => { + this.domains = domains; + }); if (!this.nodeId) { this.node = this.new(); @@ -62,6 +67,7 @@ export class ShortUrlFormPageComponent extends FormPageBase< description: new FormControl(undefined), loadingScreen: new FormControl(undefined), groupId: new FormControl(undefined), + domainId: new FormControl(undefined), }); this.form.controls['id'].disable(); } @@ -86,6 +92,7 @@ export class ShortUrlFormPageComponent extends FormPageBase< this.form.controls['description'].setValue(node?.description); this.form.controls['loadingScreen'].setValue(node?.loadingScreen); this.form.controls['groupId'].setValue(node?.group?.id); + this.form.controls['domainId'].setValue(node?.domain?.id); } getCreateInput(): ShortUrlCreateInput { @@ -103,6 +110,9 @@ export class ShortUrlFormPageComponent extends FormPageBase< groupId: this.form.controls['groupId'].pristine ? undefined : (this.form.controls['groupId'].value ?? undefined), + domainId: this.form.controls['domainId'].pristine + ? undefined + : (this.form.controls['domainId'].value ?? undefined), }; } @@ -128,6 +138,9 @@ export class ShortUrlFormPageComponent extends FormPageBase< groupId: this.form.controls['groupId'].pristine ? undefined : (this.form.controls['groupId'].value ?? undefined), + domainId: this.form.controls['domainId'].pristine + ? undefined + : (this.form.controls['domainId'].value ?? undefined), }; } diff --git a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts index 7b865f8..f43f3da 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.data.service.ts +++ b/web/src/app/modules/admin/short-urls/short-urls.data.service.ts @@ -20,6 +20,7 @@ import { ShortUrlUpdateInput, } from 'src/app/model/entities/short-url'; import { Group } from 'src/app/model/entities/group'; +import { Domain } from 'src/app/model/entities/domain'; @Injectable() export class ShortUrlsDataService @@ -66,6 +67,10 @@ export class ShortUrlsDataService id name } + domain { + id + name + } ...DB_MODEL } @@ -106,6 +111,10 @@ export class ShortUrlsDataService id name } + domain { + id + name + } ...DB_MODEL } @@ -150,6 +159,7 @@ export class ShortUrlsDataService description: object.description, loadingScreen: object.loadingScreen, groupId: object.groupId, + domainId: object.domainId, }, }, }) @@ -187,6 +197,7 @@ export class ShortUrlsDataService description: object.description, loadingScreen: object.loadingScreen, groupId: object.groupId, + domainId: object.domainId, }, }, }) @@ -268,6 +279,29 @@ export class ShortUrlsDataService .pipe(map(result => result.data.groups.nodes)); } + getAllDomains() { + return this.apollo + .query<{ domains: QueryResult }>({ + query: gql` + query getGroups { + domains { + nodes { + id + name + } + } + } + `, + }) + .pipe( + catchError(err => { + this.spinner.hide(); + throw err; + }) + ) + .pipe(map(result => result.data.domains.nodes)); + } + static provide(): Provider[] { return [ { diff --git a/web/src/app/modules/admin/short-urls/short-urls.page.html b/web/src/app/modules/admin/short-urls/short-urls.page.html index 9515224..6d87a17 100644 --- a/web/src/app/modules/admin/short-urls/short-urls.page.html +++ b/web/src/app/modules/admin/short-urls/short-urls.page.html @@ -22,6 +22,10 @@
+
+ {{ 'common.domain' | translate }}: + {{ url.domain?.name }} +
diff --git a/web/src/app/service/sidebar.service.ts b/web/src/app/service/sidebar.service.ts index 6c9d162..028b87c 100644 --- a/web/src/app/service/sidebar.service.ts +++ b/web/src/app/service/sidebar.service.ts @@ -30,6 +30,14 @@ export class SidebarService { // trust me, you'll need this async async setElements() { const elements: MenuElement[] = [ + { + label: 'common.domains', + icon: 'pi pi-sitemap', + routerLink: ['/admin/domains'], + visible: await this.auth.hasAnyPermissionLazy([ + PermissionsEnum.domains, + ]), + }, { label: 'common.groups', icon: 'pi pi-tags', diff --git a/web/src/assets/i18n/de.json b/web/src/assets/i18n/de.json index c007eea..63315f2 100644 --- a/web/src/assets/i18n/de.json +++ b/web/src/assets/i18n/de.json @@ -25,6 +25,8 @@ "created": "Erstellt", "deleted": "Gelöscht", "description": "Beschreibung", + "domain": "Domain", + "domains": "Domains", "download": "Herunterladen", "edited_at": "Bearbeitet am", "editor": "Bearbeiter", @@ -58,6 +60,9 @@ }, "save": "Speichern" }, + "domain": { + "count_header": "Domain(s)" + }, "error": { "404": "404 - Nicht gefunden", "create_failed": "Erstellung fehlgeschlagen", diff --git a/web/src/assets/i18n/en.json b/web/src/assets/i18n/en.json index 72595c2..4598ba3 100644 --- a/web/src/assets/i18n/en.json +++ b/web/src/assets/i18n/en.json @@ -25,6 +25,8 @@ "created": "Created", "deleted": "Deleted", "description": "Description", + "domain": "Domain", + "domains": "Domains", "download": "Download", "edited_at": "Edited at", "editor": "Editor", @@ -58,6 +60,9 @@ }, "save": "Save" }, + "domain": { + "count_header": "Domain(s)" + }, "error": { "404": "404 - Not found", "create_failed": "Create failed", diff --git a/web/src/styles/theme.scss b/web/src/styles/theme.scss index 6c9f760..c22df45 100644 --- a/web/src/styles/theme.scss +++ b/web/src/styles/theme.scss @@ -18,7 +18,6 @@ background-color: $backgroundColor2; - @layer utilities { .highlight { color: $textColorHighlight; @@ -59,6 +58,10 @@ .deleted { color: $accentColor !important; } + + .divider { + border-bottom: 1px solid $accentColor; + } } h1, @@ -67,6 +70,10 @@ color: $headerColor; } + input, .p-checkbox-box, .p-dropdown { + border: 1px solid $accentColor; + } + .app { .component { background-color: $backgroundColor; diff --git a/web/src/styles/theme_maxlan.scss b/web/src/styles/theme_maxlan.scss index f1ee3cb..bf27ab8 100644 --- a/web/src/styles/theme_maxlan.scss +++ b/web/src/styles/theme_maxlan.scss @@ -18,7 +18,6 @@ background-color: $backgroundColor2; - @layer utilities { .highlight { color: $textColorHighlight; @@ -59,6 +58,10 @@ .deleted { color: $accentColor !important; } + + .divider { + border-bottom: 1px solid $accentColor; + } } h1, @@ -67,6 +70,10 @@ color: $headerColor; } + input, .p-checkbox-box, .p-dropdown { + border: 1px solid $accentColor; + } + .app { .component { background-color: $backgroundColor;