From 93226022b2cb96c63564bb17c0b608634d2b349a Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Mon, 27 May 2024 09:49:47 +0700 Subject: [PATCH] refactored control panel --- control-panel/build.gradle | 1 - .../picker/cp/component/AccountList.java | 72 +++++---- .../picker/cp/component/AddEntities.java | 116 ++++++++++++++ .../picker/cp/component/AddUsers.java | 117 -------------- .../picker/cp/component/EntityList.java | 119 ++++++++++++++ .../{NodeList.java => InstanceList.java} | 83 +++++----- .../picker/cp/component/NavPath.java | 18 +-- ...egisterNode.java => RegisterInstance.java} | 65 +++++--- .../picker/cp/component/RegisterRoom.java | 34 ++-- .../picker/cp/component/RoomList.java | 98 ++++++------ .../picker/cp/component/UserList.java | 130 --------------- .../picker/cp/config/RestApiConfig.java | 23 --- .../picker/cp/config/RoomPickerConfig.java | 20 --- .../picker/cp/config/SecurityConfig.java | 56 ------- .../cp/error/ApplicationErrorHandler.java | 20 +-- .../picker/cp/exception/Unauthorized.java | 3 + .../cp/listener/VaadinEventListener.java | 2 - .../dragonestia/picker/cp/model/Account.java | 74 --------- .../picker/cp/model/AccountSession.java | 21 +++ .../picker/cp/model/Permission.java | 34 ---- .../cp/model/provider/AccountProvider.java | 10 -- .../picker/cp/page/AccountsPage.java | 29 ---- .../dragonestia/picker/cp/page/LoginPage.java | 66 -------- .../picker/cp/page/NodeDetailsPage.java | 77 --------- .../dragonestia/picker/cp/page/NodesPage.java | 56 ------- .../picker/cp/page/RoomDetailsPage.java | 151 ------------------ .../picker/cp/page/UserDetailsPage.java | 96 ----------- .../picker/cp/page/UserSearchPage.java | 115 ------------- .../page/plug/InvalidNodeIdentifierPlug.java | 20 --- .../page/plug/InvalidRoomIdentifierPlug.java | 21 --- .../picker/cp/page/plug/NodeNotFoundPlug.java | 20 --- .../picker/cp/page/plug/RoomNotFoundPlug.java | 21 --- .../cp/repository/InstanceRepository.java | 10 ++ .../picker/cp/repository/dto/EntityDTO.java | 8 + .../picker/cp/repository/dto/InstanceDTO.java | 14 ++ .../picker/cp/repository/dto/RoomDTO.java | 16 ++ .../cp/repository/graphql/AllEntities.java | 42 +++++ .../cp/repository/graphql/AllInstances.java | 37 +++++ .../cp/repository/graphql/AllRooms.java | 42 +++++ .../cp/repository/graphql/EntityData.java | 32 ++++ .../cp/repository/graphql/EntityRooms.java | 49 ++++++ .../cp/repository/graphql/SearchEntity.java | 34 ++++ .../impl/InstanceRepositoryImpl.java | 22 +++ .../picker/cp/service/AccountService.java | 25 --- .../picker/cp/service/SecurityService.java | 36 ----- .../picker/cp/service/SessionService.java | 40 +++++ .../picker/cp/util/AdminRoomPickerClient.java | 29 ---- .../cp/{component => util}/Notifications.java | 2 +- .../picker/cp/util/PermissionDescription.java | 23 +++ .../picker/cp/util/RouteParamExtractor.java | 49 ++++++ .../picker/cp/util/RouteParamsExtractor.java | 46 ------ .../picker/cp/util/UsingSlots.java | 12 ++ .../AccountView.java} | 68 ++++---- .../picker/cp/view/AllAccountsView.java | 25 +++ .../picker/cp/view/AllInstancesView.java | 55 +++++++ .../picker/cp/view/EntityView.java | 80 ++++++++++ .../picker/cp/view/InstanceView.java | 60 +++++++ .../dragonestia/picker/cp/view/LoginView.java | 81 ++++++++++ .../dragonestia/picker/cp/view/RoomView.java | 127 +++++++++++++++ .../picker/cp/view/SearchEntityView.java | 110 +++++++++++++ .../picker/cp/view/SecuredView.java | 84 ++++++++++ .../cp/{page => view/layout}/MainLayout.java | 61 ++++--- .../cp/{page => view}/plug/ErrorPlug.java | 7 +- .../cp/view/plug/InvalidIdentifierPlug.java | 17 ++ .../plug/NotEnoughPermissionsPlug.java | 3 +- .../picker/cp/view/plug/NotFoundPlug.java | 17 ++ .../picker/aspect/UserMetricsAspect.java | 8 +- 67 files changed, 1620 insertions(+), 1539 deletions(-) create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddEntities.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddUsers.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/component/EntityList.java rename control-panel/src/main/java/ru/dragonestia/picker/cp/component/{NodeList.java => InstanceList.java} (58%) rename control-panel/src/main/java/ru/dragonestia/picker/cp/component/{RegisterNode.java => RegisterInstance.java} (65%) delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/component/UserList.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/config/RestApiConfig.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/config/SecurityConfig.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/exception/Unauthorized.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/model/Account.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/model/AccountSession.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/model/Permission.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/model/provider/AccountProvider.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountsPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/LoginPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodeDetailsPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodesPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/RoomDetailsPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserDetailsPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserSearchPage.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidNodeIdentifierPlug.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidRoomIdentifierPlug.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NodeNotFoundPlug.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/RoomNotFoundPlug.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/InstanceRepository.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/EntityDTO.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/InstanceDTO.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/RoomDTO.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllEntities.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllInstances.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllRooms.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityData.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityRooms.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/SearchEntity.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/repository/impl/InstanceRepositoryImpl.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/service/AccountService.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/service/SecurityService.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/service/SessionService.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/util/AdminRoomPickerClient.java rename control-panel/src/main/java/ru/dragonestia/picker/cp/{component => util}/Notifications.java (97%) create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/util/PermissionDescription.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamExtractor.java delete mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamsExtractor.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/util/UsingSlots.java rename control-panel/src/main/java/ru/dragonestia/picker/cp/{page/AccountDetailsPage.java => view/AccountView.java} (60%) create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllAccountsView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllInstancesView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/EntityView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/InstanceView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/LoginView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/RoomView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/SearchEntityView.java create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/SecuredView.java rename control-panel/src/main/java/ru/dragonestia/picker/cp/{page => view/layout}/MainLayout.java (51%) rename control-panel/src/main/java/ru/dragonestia/picker/cp/{page => view}/plug/ErrorPlug.java (69%) create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/InvalidIdentifierPlug.java rename control-panel/src/main/java/ru/dragonestia/picker/cp/{page => view}/plug/NotEnoughPermissionsPlug.java (89%) create mode 100644 control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotFoundPlug.java diff --git a/control-panel/build.gradle b/control-panel/build.gradle index 6b5bf5d..07d424d 100644 --- a/control-panel/build.gradle +++ b/control-panel/build.gradle @@ -30,7 +30,6 @@ dependencies { implementation project(":client-impl") implementation 'com.vaadin:vaadin-spring-boot-starter' - implementation 'org.springframework.boot:spring-boot-starter-security' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AccountList.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AccountList.java index 8f1c0b1..e79e611 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AccountList.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AccountList.java @@ -17,23 +17,25 @@ import com.vaadin.flow.component.textfield.PasswordField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.theme.lumo.LumoIcon; -import ru.dragonestia.picker.api.model.account.ResponseAccount; -import ru.dragonestia.picker.api.repository.AccountRepository; -import ru.dragonestia.picker.cp.model.Permission; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.account.Account; +import ru.dragonestia.picker.api.model.account.AccountId; +import ru.dragonestia.picker.api.model.account.Permission; +import ru.dragonestia.picker.cp.util.PermissionDescription; +import ru.dragonestia.picker.cp.util.Notifications; import java.util.*; -import java.util.stream.Collectors; public class AccountList extends VerticalLayout implements RefreshableTable { - private final AccountRepository accountRepository; + private final RoomPickerClient client; private final TextField searchField; - private final Grid grid; + private final Grid grid; - private List cachedAccounts = new ArrayList<>(); + private List cachedAccounts = new ArrayList<>(); - public AccountList(AccountRepository accountRepository) { - this.accountRepository = accountRepository; + public AccountList(RoomPickerClient client) { + this.client = client; add(searchField = createSearchField()); add(grid = createGridAccounts()); @@ -42,7 +44,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable { } private TextField createSearchField() { - var field = new TextField("Search by account username"); + var field = new TextField("Search by account entityname"); field.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); field.setClearButtonVisible(true); field.setHelperText("Press Enter to search"); @@ -52,11 +54,11 @@ public class AccountList extends VerticalLayout implements RefreshableTable { return field; } - private Grid createGridAccounts() { - var grid = new Grid<>(ResponseAccount.class, false); + private Grid createGridAccounts() { + var grid = new Grid<>(Account.class, false); - grid.addColumn(ResponseAccount::getUsername).setHeader("Username") - .setComparator(Comparator.comparing(ResponseAccount::getUsername)).setSortable(true); + grid.addColumn(Account::id).setHeader("Username") + .setComparator(Comparator.comparing(account -> account.id().getValue())).setSortable(true); grid.addComponentColumn(this::createAccountManagementButtons).setFrozenToEnd(true) .setTextAlign(ColumnTextAlign.END).setHeader(createToolItems()); @@ -66,7 +68,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable { return grid; } - private HorizontalLayout createAccountManagementButtons(ResponseAccount account) { + private HorizontalLayout createAccountManagementButtons(Account account) { var layout = new HorizontalLayout(JustifyContentMode.END); { @@ -74,7 +76,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable { button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addClickListener(event -> { getUI().ifPresent(ui -> { - ui.navigate("/admin/accounts/" + account.getUsername()); + ui.navigate("/admin/accounts/" + account.id()); }); }); layout.add(button); @@ -101,7 +103,8 @@ public class AccountList extends VerticalLayout implements RefreshableTable { @Override public void refresh() { - cachedAccounts = accountRepository.allAccounts(); + var ids = client.getAccountRepository().allAccountsIds(); + cachedAccounts = client.getAccountRepository().getAccounts(ids); applySearch(searchField.getValue()); } @@ -109,7 +112,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable { var temp = input.trim(); grid.setItems(cachedAccounts.stream() - .filter(account -> account.getUsername().startsWith(temp)) + .filter(account -> account.id().getValue().startsWith(temp)) .toList()); } @@ -123,9 +126,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable { var layout = new VerticalLayout(); - var fieldUsername = new TextField("Account username"); - fieldUsername.setWidth(70, Unit.PERCENTAGE); - layout.add(fieldUsername); + var fieldEntityname = new TextField("Username"); + fieldEntityname.setWidth(70, Unit.PERCENTAGE); + layout.add(fieldEntityname); var fieldPassword = new PasswordField("Password"); fieldPassword.setWidth(70, Unit.PERCENTAGE); @@ -138,7 +141,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable { layout.add(new H3("Permissions")); var permissionsList = new ArrayList(); - for (var permission: Permission.Enum.values()) { + for (var permission: Permission.values()) { + if (permission == Permission.ADMIN) continue; + var comp = new PermissionCheckBox(permission); permissionsList.add(comp); layout.add(comp); @@ -149,7 +154,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable { button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.setWidth(100, Unit.PERCENTAGE); button.addClickListener(event -> { - validateAndRegister(dialog, fieldUsername, fieldPassword, fieldConfirmPassword, permissionsList); + validateAndRegister(dialog, fieldEntityname, fieldPassword, fieldConfirmPassword, permissionsList); }); dialog.getFooter().add(button); } @@ -162,13 +167,13 @@ public class AccountList extends VerticalLayout implements RefreshableTable { dialog.open(); } - private void validateAndRegister(Dialog dialog, TextField usernameField, PasswordField passwordField, PasswordField confirmPasswordField, List permissionCheckBoxes) { - var username = usernameField.getValue().trim(); + private void validateAndRegister(Dialog dialog, TextField entitynameField, PasswordField passwordField, PasswordField confirmPasswordField, List permissionCheckBoxes) { + var entityname = entitynameField.getValue().trim(); var password = passwordField.getValue(); var confirmPassword = confirmPasswordField.getValue(); - if (username.length() < 3 || username.length() > 32) { - Notifications.error("Invalid username length. Valid is 3-32"); + if (entityname.length() < 3 || entityname.length() > 32) { + Notifications.error("Invalid entityname length. Valid is 3-32"); return; } @@ -185,10 +190,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable { var permissions = permissionCheckBoxes.stream() .filter(AbstractField::getValue) .map(PermissionCheckBox::getOption) - .map(Enum::name) - .collect(Collectors.toSet()); + .toList(); - accountRepository.createAccount(username, password, permissions); + client.getAccountRepository().createAccount(AccountId.of(entityname), password, permissions); dialog.close(); refresh(); @@ -196,14 +200,14 @@ public class AccountList extends VerticalLayout implements RefreshableTable { public static class PermissionCheckBox extends Checkbox { - private final Permission.Enum option; + private final Permission option; - public PermissionCheckBox(Permission.Enum option) { - super(option.getDescription()); + public PermissionCheckBox(Permission option) { + super(PermissionDescription.of(option)); this.option = option; } - public Permission.Enum getOption() { + public Permission getOption() { return option; } } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddEntities.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddEntities.java new file mode 100644 index 0000000..4acb5f6 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddEntities.java @@ -0,0 +1,116 @@ +package ru.dragonestia.picker.cp.component; + +import com.vaadin.flow.component.Unit; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.details.Details; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import lombok.Getter; +import ru.dragonestia.picker.api.model.entity.EntityId; +import ru.dragonestia.picker.api.model.room.Room; +import ru.dragonestia.picker.cp.util.Notifications; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; + +public class AddEntities extends Details { + + private final BiConsumer, Boolean> onCommit; + private final Checkbox ignoreSlots; + private final VerticalLayout entitiesLayout; + private final AtomicInteger freeEntityIdNumber = new AtomicInteger(1); + + public AddEntities(Room room, BiConsumer, Boolean> onCommit) { + super(new H2("Add entities")); + + this.onCommit = onCommit; + entitiesLayout = new VerticalLayout(); + + add(addEntityToTransacionButton()); + add(entitiesLayout); + entitiesLayout.add(new EntityEntry(false, freeEntityIdNumber.getAndIncrement())); + add(ignoreSlots = new Checkbox("Ignore slot limitation", false)); + add(createAddEntitiesButton()); + } + + public void clear() { + freeEntityIdNumber.set(1); + ignoreSlots.setValue(false); + entitiesLayout.removeAll(); + entitiesLayout.add(new EntityEntry(false, freeEntityIdNumber.getAndIncrement())); + } + + public List readAllEntities() { + return entitiesLayout.getChildren() + .filter(component -> component instanceof EntityEntry) + .map(component -> (EntityEntry) component) + .map(entity -> entity.getEntityIdentifierField().getValue()) + .map(String::trim) + .filter(entity -> !entity.isEmpty()) + .map(EntityId::of) + .toList(); + } + + private Button addEntityToTransacionButton() { + var button = new Button("Add entity to transaction"); + button.addClickListener(event -> { + entitiesLayout.add(new EntityEntry(true, freeEntityIdNumber.getAndIncrement())); + }); + button.setPrefixComponent(new Icon(VaadinIcon.PLUS)); + return button; + } + + private Button createAddEntitiesButton() { + var button = new Button("Commit", event -> onClick()); + button.addThemeVariants(ButtonVariant.LUMO_SUCCESS, ButtonVariant.LUMO_PRIMARY); + return button; + } + + private void onClick() { + try { + onCommit.accept(readAllEntities(), ignoreSlots.getValue()); + } catch (Error error) { + Notifications.error(error.getMessage()); + } + + clear(); + } + + @Getter + public static class EntityEntry extends Div { + + private final TextField entityIdentifierField; + + public EntityEntry(boolean canBeDeleted, int number) { + add(entityIdentifierField = createEntityIdentifierField(canBeDeleted, number)); + } + + private TextField createEntityIdentifierField(boolean canBeDeleted, int number) { + var field = new TextField("Entity id"); + field.setPlaceholder("example-entity-id-" + number); + if (!canBeDeleted) { + field.setHelperText("It can be UUID, entityname, numeric ids, etc"); + } + field.setMinWidth(20, Unit.REM); + + if (canBeDeleted) { + var removeButton = new Button(new Icon(VaadinIcon.TRASH), event -> { + removeFromParent(); + }); + removeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + + field.setSuffixComponent(removeButton); + } + + return field; + } + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddUsers.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddUsers.java deleted file mode 100644 index 9be3cdf..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/AddUsers.java +++ /dev/null @@ -1,117 +0,0 @@ -package ru.dragonestia.picker.cp.component; - -import com.vaadin.flow.component.Unit; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.checkbox.Checkbox; -import com.vaadin.flow.component.details.Details; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.TextField; -import lombok.Getter; -import ru.dragonestia.picker.api.model.room.IRoom; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.model.user.UserDefinition; -import ru.dragonestia.picker.api.repository.type.EntityIdentifier; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; - -public class AddUsers extends Details { - - private final BiConsumer, Boolean> onCommit; - private final Checkbox ignoreSlots; - private final VerticalLayout usersLayout; - private final AtomicInteger freeUserIdNumber = new AtomicInteger(1); - - public AddUsers(IRoom room, BiConsumer, Boolean> onCommit) { - super(new H2("Add users")); - - this.onCommit = onCommit; - usersLayout = new VerticalLayout(); - - add(addUserToTransacionButton()); - add(usersLayout); - usersLayout.add(new UserEntry(false, freeUserIdNumber.getAndIncrement())); - add(ignoreSlots = new Checkbox("Ignore slot limitation", false)); - add(createAddUsersButton()); - } - - public void clear() { - freeUserIdNumber.set(1); - ignoreSlots.setValue(false); - usersLayout.removeAll(); - usersLayout.add(new UserEntry(false, freeUserIdNumber.getAndIncrement())); - } - - public List readAllUsers() { - return usersLayout.getChildren() - .filter(component -> component instanceof UserEntry) - .map(component -> (UserEntry) component) - .map(user -> user.getUserIdentifierField().getValue()) - .map(String::trim) - .filter(user -> !user.isEmpty()) - .map(id -> (IUser) new UserDefinition(EntityIdentifier.of(id))) - .toList(); - } - - private Button addUserToTransacionButton() { - var button = new Button("Add user to transaction"); - button.addClickListener(event -> { - usersLayout.add(new UserEntry(true, freeUserIdNumber.getAndIncrement())); - }); - button.setPrefixComponent(new Icon(VaadinIcon.PLUS)); - return button; - } - - private Button createAddUsersButton() { - var button = new Button("Commit", event -> onClick()); - button.addThemeVariants(ButtonVariant.LUMO_SUCCESS, ButtonVariant.LUMO_PRIMARY); - return button; - } - - private void onClick() { - try { - onCommit.accept(readAllUsers(), ignoreSlots.getValue()); - } catch (Error error) { - Notifications.error(error.getMessage()); - } - - clear(); - } - - @Getter - public static class UserEntry extends Div { - - private final TextField userIdentifierField; - - public UserEntry(boolean canBeDeleted, int number) { - add(userIdentifierField = createUserIdentifierField(canBeDeleted, number)); - } - - private TextField createUserIdentifierField(boolean canBeDeleted, int number) { - var field = new TextField("User id"); - field.setPlaceholder("example-user-id-" + number); - if (!canBeDeleted) { - field.setHelperText("It can be UUID, username, numeric ids, etc"); - } - field.setMinWidth(20, Unit.REM); - - if (canBeDeleted) { - var removeButton = new Button(new Icon(VaadinIcon.TRASH), event -> { - removeFromParent(); - }); - removeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); - - field.setSuffixComponent(removeButton); - } - - return field; - } - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/EntityList.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/EntityList.java new file mode 100644 index 0000000..7309f28 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/EntityList.java @@ -0,0 +1,119 @@ +package ru.dragonestia.picker.cp.component; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.entity.EntityId; +import ru.dragonestia.picker.api.model.room.Room; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; +import ru.dragonestia.picker.cp.repository.graphql.AllEntities; +import ru.dragonestia.picker.cp.util.UsingSlots; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class EntityList extends VerticalLayout implements RefreshableTable { + + private final Room room; + private final RoomPickerClient client; + private final Button buttonRemove; + private final Grid entitiesGrid; + private final Span totalEntities = new Span(); + private final Span occupancy = new Span(); + private List cachedEntities = new ArrayList<>(); + + public EntityList(Room room, RoomPickerClient client) { + this.room = room; + this.client = client; + + buttonRemove = createButtonRemove(); + add(entitiesGrid = createEntitiesGrid()); + + refresh(); + updateButtonRemove(); + } + + private Button createButtonRemove() { + var button = new Button("Unlink"); + button.setPrefixComponent(new Icon(VaadinIcon.UNLINK)); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + button.addClickListener(event -> { + var entities = entitiesGrid.getSelectedItems(); + if (entities.isEmpty()) return; + client.getEntityRepository().unlinkEntitiesFromRoom(room, entities.stream().map(entity -> EntityId.of(entity.getId())).collect(Collectors.toSet())); + refresh(); + }); + return button; + } + + private Grid createEntitiesGrid() { + var grid = new Grid(); + + grid.addColumn(EntityDTO::getId).setHeader("Entity Identifier").setSortable(true).setFooter(totalEntities); + + grid.addColumn(EntityDTO::getCountRooms).setTextAlign(ColumnTextAlign.CENTER) + .setHeader("Linked with rooms").setComparator((entity1, entity2) -> { + var r1 = entity1.getCountRooms(); + var r2 = entity2.getCountRooms(); + + return Integer.compare(r1, r2); + }).setSortable(true).setFooter(occupancy); + + grid.addComponentColumn(this::createManageButton).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true) + .setTextAlign(ColumnTextAlign.END).setHeader(createManageTableButtons()); + + grid.setSelectionMode(Grid.SelectionMode.MULTI); + grid.addSelectionListener(event -> updateButtonRemove()); + grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); + return grid; + } + + private Button createManageButton(EntityDTO entity) { + var button = new Button("Details"); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.addClickListener(e -> { + getUI().ifPresent(ui -> ui.navigate("/entities/" + entity.getId())); + }); + return button; + } + + private HorizontalLayout createManageTableButtons() { + var layout = new HorizontalLayout(); + layout.setJustifyContentMode(JustifyContentMode.END); + + layout.add(buttonRemove); + layout.add(createRefreshButton()); + + return layout; + } + + private void updateButtonRemove() { + var entities = entitiesGrid.getSelectedItems(); + + if (entities.isEmpty()) { + buttonRemove.setEnabled(false); + buttonRemove.setText("Unlink"); + return; + } + + buttonRemove.setEnabled(true); + buttonRemove.setText("Unlink(" + entities.size() + ")"); + } + + @Override + public void refresh() { + cachedEntities = client.getRestTemplate().executeGraphQL(AllEntities.query(room.instanceId().getValue(), room.id().getValue())) + .getRoomById().getEntities().stream().map(entity -> (EntityDTO) entity).toList(); + entitiesGrid.setItems(cachedEntities); + totalEntities.setText("Total entities: " + cachedEntities.size()); + occupancy.setText("Occupancy: %s".formatted(UsingSlots.getUsingPercentage(room.slots(), cachedEntities.size()) + "%")); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NodeList.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/InstanceList.java similarity index 58% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/component/NodeList.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/component/InstanceList.java index da9e1b6..0c0d58e 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NodeList.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/InstanceList.java @@ -14,34 +14,34 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.value.ValueChangeMode; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.api.model.node.NodeDetails; -import ru.dragonestia.picker.api.repository.InstanceRepository; -import ru.dragonestia.picker.api.repository.query.node.GetAllNodes; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.instance.InstanceId; +import ru.dragonestia.picker.cp.repository.dto.InstanceDTO; +import ru.dragonestia.picker.cp.repository.graphql.AllInstances; +import ru.dragonestia.picker.cp.util.Notifications; import java.util.Comparator; import java.util.List; -public class NodeList extends VerticalLayout implements RefreshableTable { +public class InstanceList extends VerticalLayout implements RefreshableTable { - private final InstanceRepository instanceRepository; - private final Grid nodesGrid; + private final RoomPickerClient client; + private final Grid instancesGrid; private final TextField searchField; - private List cachedNodes; + private List cachedInstances; - public NodeList(InstanceRepository instanceRepository) { - super(); - this.instanceRepository = instanceRepository; + public InstanceList(RoomPickerClient client) { + this.client = client; - add(new H2("Nodes")); + add(new H2("Instances")); add(searchField = createSearchField()); - add(nodesGrid = createGrid()); + add(instancesGrid = createGrid()); refresh(); } private TextField createSearchField() { - var field = new TextField("Search node"); + var field = new TextField("Search instance"); field.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); field.setClearButtonVisible(true); field.setHelperText("Press Enter to search"); @@ -53,25 +53,28 @@ public class NodeList extends VerticalLayout implements RefreshableTable { private void applySearch(String input) { var temp = input.trim(); - nodesGrid.setItems(cachedNodes.stream() - .filter(node -> node.getIdentifier().startsWith(temp)) - .toList()); + var instances = cachedInstances.stream() + .filter(instance -> instance.getId().startsWith(temp)) + .map(instance -> (InstanceDTO) instance) + .toList(); + + instancesGrid.setItems(instances); } - private Grid createGrid() { - var grid = new Grid<>(INode.class, false); + private Grid createGrid() { + var grid = new Grid<>(InstanceDTO.class, false); - grid.addComponentColumn(node -> { - if (Boolean.parseBoolean(node.getDetail(NodeDetails.PERSIST))) { - return new Span(node.getIdentifier()); + grid.addComponentColumn(instance -> { + if (instance.isPersist()) { + return new Span(instance.getId()); } - var result = new Span(node.getIdentifier()); + var result = new Span(instance.getId()); result.add(grayBadge("(temp)")); return result; - }).setHeader("Identifier").setComparator(Comparator.comparing(INode::getIdentifier)).setSortable(true); + }).setHeader("Identifier").setComparator(Comparator.comparing(InstanceDTO::getId)).setSortable(true); - grid.addColumn(node -> node.getPickingMethod().name()).setHeader("Mode").setSortable(true); + grid.addColumn(instance -> instance.getMethod().name()).setHeader("Mode").setSortable(true); grid.addComponentColumn(this::createManageButtons).setFrozenToEnd(true) .setTextAlign(ColumnTextAlign.END).setHeader(createRefreshButton()); @@ -80,33 +83,33 @@ public class NodeList extends VerticalLayout implements RefreshableTable { return grid; } - private HorizontalLayout createManageButtons(INode node) { + private HorizontalLayout createManageButtons(InstanceDTO instance) { var layout = new HorizontalLayout(JustifyContentMode.END); { var button = new Button("Details"); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - button.addClickListener(event -> clickDetailsButton(node)); + button.addClickListener(event -> clickDetailsButton(instance)); layout.add(button); } { var button = new Button("Remove"); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); - button.addClickListener(event -> clickRemoveButton(node)); + button.addClickListener(event -> clickRemoveButton(instance)); layout.add(button); } return layout; } - private void clickDetailsButton(INode node) { - getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.getIdentifier())); + private void clickDetailsButton(InstanceDTO instance) { + getUI().ifPresent(ui -> ui.navigate("/instances/" + instance.getId())); } - private void clickRemoveButton(INode node) { - var dialog = new Dialog("Confirm node deletion"); - dialog.add(new Html("

Confirm that you want to delete node. Enter " + node.getIdentifier() + " to field below and confirm.

")); + private void clickRemoveButton(InstanceDTO instance) { + var dialog = new Dialog("Confirm instance deletion"); + dialog.add(new Html("

Confirm that you want to delete instance. Enter " + instance.getId() + " to field below and confirm.

")); var inputField = new TextField(); inputField.setWidth("100%"); @@ -116,13 +119,13 @@ public class NodeList extends VerticalLayout implements RefreshableTable { var button = new Button("Confirm"); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addClickListener(event -> { - if (!node.getIdentifier().equals(inputField.getValue())) { + if (!instance.getId().equals(inputField.getValue())) { Notifications.error("Invalid input"); return; } - removeNode(node); - Notifications.success("Node " + node.getIdentifier() + " was successfully removed!"); + removeInstance(instance); + Notifications.success("Instance " + instance.getId() + " was successfully removed!"); dialog.close(); }); @@ -138,14 +141,16 @@ public class NodeList extends VerticalLayout implements RefreshableTable { dialog.open(); } - private void removeNode(INode node) { - instanceRepository.removeNode(node); + private void removeInstance(InstanceDTO instance) { + client.getInstanceRepository().deleteInstance(InstanceId.of(instance.getId())); refresh(); } @Override public void refresh() { - cachedNodes = instanceRepository.allNodes(GetAllNodes.WITH_ALL_DETAILS); + cachedInstances = client.getRestTemplate().executeGraphQL(AllInstances.query()).getAllInstances().stream() + .map(instance -> (InstanceDTO) instance) + .toList(); applySearch(searchField.getValue()); } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NavPath.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NavPath.java index f9af319..b2c0ef7 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NavPath.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/NavPath.java @@ -58,19 +58,19 @@ public class NavPath extends HorizontalLayout{ return button; } - public static NavPath rootNodes() { - return new NavPath(new NavPath.Point("Nodes", "/nodes")); + public static NavPath rootInstances() { + return new NavPath(new NavPath.Point("Instances", "/instances")); } - public static NavPath toNode(String nodeId) { - return new NavPath(new NavPath.Point("Nodes", "/nodes"), - new NavPath.Point(nodeId, "/nodes/" + nodeId)); + public static NavPath toInstance(String instanceId) { + return new NavPath(new NavPath.Point("Instances", "/instances"), + new NavPath.Point(instanceId, "/instances/" + instanceId)); } - public static NavPath toRoom(String nodeId, String roomId) { - return new NavPath(new NavPath.Point("Nodes", "/nodes"), - new NavPath.Point(nodeId, "/nodes/" + nodeId), - new NavPath.Point(roomId, "/nodes/" + nodeId + "/rooms/" + roomId)); + public static NavPath toRoom(String instanceId, String roomId) { + return new NavPath(new NavPath.Point("Instances", "/instances"), + new NavPath.Point(instanceId, "/instances/" + instanceId), + new NavPath.Point(roomId, "/instances/" + instanceId + "/rooms/" + roomId)); } private record Point(String name, String uri) {} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterNode.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterInstance.java similarity index 65% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterNode.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterInstance.java index c9901d1..9aedcc1 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterNode.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterInstance.java @@ -15,25 +15,27 @@ import com.vaadin.flow.component.textfield.Autocomplete; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.renderer.ComponentRenderer; import org.springframework.lang.Nullable; -import ru.dragonestia.picker.api.model.node.NodeDefinition; -import ru.dragonestia.picker.api.model.node.PickingMethod; -import ru.dragonestia.picker.api.repository.type.NodeIdentifier; +import ru.dragonestia.picker.api.exception.InvalidIdentifierException; +import ru.dragonestia.picker.api.model.instance.InstanceId; +import ru.dragonestia.picker.api.model.instance.type.PickingMethod; +import ru.dragonestia.picker.cp.repository.dto.InstanceDTO; +import ru.dragonestia.picker.cp.util.Notifications; import java.util.function.Function; -public class RegisterNode extends Details { +public class RegisterInstance extends Details { - private final Function onSubmit; + private final Function onSubmit; private final TextField identifierField; private final RadioButtonGroup modeRadio; private final Checkbox persistField; - public RegisterNode(Function onSubmit) { - super(new H2("Register node")); + public RegisterInstance(Function onSubmit) { + super(new H2("Register instance")); this.onSubmit = onSubmit; var layout = new VerticalLayout(); - layout.add(identifierField = createNodeIdentifierField()); + layout.add(identifierField = createInstanceIdentifierField()); layout.add(modeRadio = createModeRadio()); layout.add(persistField = createPersistField()); layout.add(createSubmitButton()); @@ -41,10 +43,10 @@ public class RegisterNode extends Details { add(layout); } - private TextField createNodeIdentifierField() { + private TextField createInstanceIdentifierField() { var field = new TextField("Identifier"); field.setMinWidth(20, Unit.REM); - field.setPlaceholder("example-node-id"); + field.setPlaceholder("example-instance-id"); field.setHelperText("The field can contain only lowercase letters, numbers and a dash character"); field.setPattern("^[a-z\\d-]+$"); field.setRequired(true); @@ -83,36 +85,61 @@ public class RegisterNode extends Details { private @Nullable String validateForm(String identifier) { if (identifier.isEmpty()) { - return "Node id cannot be empty"; + return "Instance id cannot be empty"; + } + + try { + InstanceId.of(identifier); + } catch (InvalidIdentifierException ex) { + return "Invalid identifier"; } return null; } private void onClick() { - String nodeIdentifier = identifierField.getValue(); + String instanceIdentifier = identifierField.getValue(); String error = null; - if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) { + if (identifierField.isInvalid() || (error = validateForm(instanceIdentifier)) != null) { if (identifierField.isInvalid()) { - error = "Invalid node id format"; + error = "Invalid instance id format"; } Notifications.error(error); return; } - var node = new NodeDefinition(NodeIdentifier.of(nodeIdentifier)) - .setPickingMethod(modeRadio.getValue()) - .setPersist(persistField.getValue()); - var response = onSubmit.apply(node); + var response = onSubmit.apply(new InstanceDTO() { + + @Override + public String getId() { + return instanceIdentifier; + } + + @Override + public PickingMethod getMethod() { + return modeRadio.getValue(); + } + + @Override + public boolean isPersist() { + return persistField.getValue(); + } + + @Override + public int getCountRooms() { + throw new UnsupportedOperationException(); + } + }); + clear(); if (response.error()) { Notifications.error(response.reason()); return; } - Notifications.success("Node was successfully registered"); + Notifications.success("Instance was successfully registered"); } public record Response(boolean error, @Nullable String reason) {} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterRoom.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterRoom.java index dcb9883..a1f8c0e 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterRoom.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RegisterRoom.java @@ -11,29 +11,29 @@ import com.vaadin.flow.component.textfield.Autocomplete; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; import org.springframework.lang.Nullable; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.api.model.room.IRoom; -import ru.dragonestia.picker.api.model.room.RoomDefinition; -import ru.dragonestia.picker.api.repository.type.RoomIdentifier; +import ru.dragonestia.picker.api.model.instance.Instance; +import ru.dragonestia.picker.api.model.room.Room; +import ru.dragonestia.picker.api.model.room.RoomId; +import ru.dragonestia.picker.cp.util.Notifications; import java.util.function.Function; public class RegisterRoom extends Details { - private final INode node; - private final Function onSubmit; + private final Instance instance; + private final Function onSubmit; private final TextField identifierField; private final TextArea payloadField; private final Checkbox lockedField; private final Checkbox persistField; - public RegisterRoom(INode node, Function onSubmit) { + public RegisterRoom(Instance instance, Function onSubmit) { super(new H2("Register room")); - this.node = node; + this.instance = instance; this.onSubmit = onSubmit; var layout = new VerticalLayout(); - layout.add(createNodeIdentifierField()); + layout.add(createInstanceIdentifierField()); layout.add(identifierField = createRoomIdentifierField()); layout.add(payloadField = createPayloadField()); layout.add(lockedField = createLockedField()); @@ -43,10 +43,10 @@ public class RegisterRoom extends Details { add(layout); } - private TextField createNodeIdentifierField() { - var field = new TextField("Node identifier"); + private TextField createInstanceIdentifierField() { + var field = new TextField("Instance identifier"); field.setMinWidth(20, Unit.REM); - field.setValue(node.getIdentifier()); + field.setValue(instance.id().getValue()); field.setReadOnly(true); return field; } @@ -97,7 +97,7 @@ public class RegisterRoom extends Details { private @Nullable String validateForm(String identifier) { if (identifier.isEmpty()) { - return "Node identifier cannot be empty"; + return "Instance identifier cannot be empty"; } return null; @@ -116,13 +116,7 @@ public class RegisterRoom extends Details { return; } - var room = new RoomDefinition(node.getIdentifierObject(), RoomIdentifier.of(roomId)) - .setMaxSlots(IRoom.UNLIMITED_SLOTS) - .setPayload(payloadField.getValue()) - .setPersist(persistField.getValue()); - - room.setLocked(lockedField.getValue()); - var response = onSubmit.apply(room); + var response = onSubmit.apply(new Room(RoomId.of(roomId), instance.id(), -1, lockedField.getValue(), payloadField.getValue(), persistField.getValue())); clear(); if (response.error()) { Notifications.error(response.reason()); diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RoomList.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RoomList.java index 271acbf..08e4540 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RoomList.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/RoomList.java @@ -15,27 +15,31 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.value.ValueChangeMode; import lombok.extern.log4j.Log4j2; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.api.model.room.RoomDetails; -import ru.dragonestia.picker.api.model.room.ShortResponseRoom; -import ru.dragonestia.picker.api.repository.RoomRepository; -import ru.dragonestia.picker.api.repository.query.room.GetAllRooms; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.instance.Instance; +import ru.dragonestia.picker.api.model.instance.InstanceId; +import ru.dragonestia.picker.api.model.room.RoomId; +import ru.dragonestia.picker.cp.repository.dto.RoomDTO; +import ru.dragonestia.picker.cp.repository.graphql.AllRooms; +import ru.dragonestia.picker.cp.util.Notifications; +import ru.dragonestia.picker.cp.util.UsingSlots; +import java.util.Comparator; import java.util.List; @Log4j2 public class RoomList extends VerticalLayout implements RefreshableTable { - private final INode node; - private final RoomRepository roomRepository; - private final Grid roomsGrid; + private final Instance instance; + private final RoomPickerClient client; + private final Grid roomsGrid; private final TextField searchField; - private List cachedRooms; - private final Span totalUsers = new Span(); + private List cachedRooms; + private final Span totalEntities = new Span(); - public RoomList(INode node, RoomRepository roomRepository) { - this.node = node; - this.roomRepository = roomRepository; + public RoomList(Instance instance, RoomPickerClient client) { + this.instance = instance; + this.client = client; add(new H2("Rooms")); add(searchField = createSearchField()); @@ -58,39 +62,39 @@ public class RoomList extends VerticalLayout implements RefreshableTable { var temp = input.trim(); roomsGrid.setItems(cachedRooms.stream() - .filter(room -> room.getIdentifier().startsWith(temp)) + .filter(room -> room.getId().startsWith(temp)) .toList()); } - private Grid createGrid() { - var grid = new Grid<>(ShortResponseRoom.class, false); + private Grid createGrid() { + var grid = new Grid<>(RoomDTO.class, false); - grid.addColumn(ShortResponseRoom::getIdentifier).setHeader("Identifier").setSortable(true); + grid.addColumn(RoomDTO::getId).setHeader("Identifier").setSortable(true); grid.addComponentColumn(room -> { var result = new Span(); - if (room.getMaxSlots() == -1) { + if (room.getSlots() == -1) { result.setText("Unlimited"); result.getElement().getThemeList().add("badge contrast"); } else { - result.setText(Integer.toString(room.getMaxSlots())); + result.setText(Integer.toString(room.getSlots())); } return result; }).setHeader("Slots").setComparator((room1, room2) -> { - var r1 = room1.hasUnlimitedSlots()? Integer.MAX_VALUE : room1.getMaxSlots(); - var r2 = room2.hasUnlimitedSlots()? Integer.MAX_VALUE : room2.getMaxSlots(); + var r1 = room1.getSlots() == 1? Integer.MAX_VALUE : room1.getSlots(); + var r2 = room2.getSlots() == 1? Integer.MAX_VALUE : room2.getSlots(); return Integer.compare(r1, r2); }).setSortable(true).setTextAlign(ColumnTextAlign.CENTER); - grid.addColumn(this::getUsers).setHeader("Users") - .setComparator((room1, room2) -> Integer.compare(getUsers(room1), getUsers(room2))).setSortable(true) - .setTextAlign(ColumnTextAlign.CENTER).setFooter(totalUsers); + grid.addColumn(RoomDTO::getCountEntities).setHeader("Entities") + .setComparator(Comparator.comparingInt(RoomDTO::getCountEntities)).setSortable(true) + .setTextAlign(ColumnTextAlign.CENTER).setFooter(totalEntities); - grid.addColumn(room -> Math.max(UserList.getUsingPercentage(room.getMaxSlots(), getUsers(room)), 0) + "%") + grid.addColumn(room -> Math.max(UsingSlots.getUsingPercentage(room.getSlots(), room.getCountEntities()), 0) + "%") .setComparator((room1, room2) -> { - var p1 = UserList.getUsingPercentage(room1.getMaxSlots(), getUsers(room1)); - var p2 = UserList.getUsingPercentage(room2.getMaxSlots(), getUsers(room2)); + var p1 = UsingSlots.getUsingPercentage(room1.getSlots(), room1.getCountEntities()); + var p2 = UsingSlots.getUsingPercentage(room2.getSlots(), room2.getCountEntities()); return Integer.compare(p1, p2); }).setHeader("Occupancy").setTextAlign(ColumnTextAlign.CENTER); @@ -114,7 +118,7 @@ public class RoomList extends VerticalLayout implements RefreshableTable { return grid; } - private HorizontalLayout createManageButtons(ShortResponseRoom room) { + private HorizontalLayout createManageButtons(RoomDTO room) { var layout = new HorizontalLayout(JustifyContentMode.END); { @@ -134,15 +138,15 @@ public class RoomList extends VerticalLayout implements RefreshableTable { return layout; } - private void clickDetailsButton(ShortResponseRoom room) { + private void clickDetailsButton(RoomDTO room) { getUI().ifPresent(ui -> { - ui.navigate("/nodes/%s/rooms/%s".formatted(node.getIdentifier(), room.getIdentifier())); + ui.navigate("/instances/%s/rooms/%s".formatted(instance.id(), room.getId())); }); } - private void clickRemoveButton(ShortResponseRoom room) { + private void clickRemoveButton(RoomDTO room) { var dialog = new Dialog("Confirm room deletion"); - dialog.add(new Html("

Confirm that you want to delete room. Enter " + room.getIdentifier() + " to field below and confirm.

")); + dialog.add(new Html("

Confirm that you want to delete room. Enter " + room.getId() + " to field below and confirm.

")); var inputField = new TextField(); inputField.setWidth("100%"); @@ -152,13 +156,13 @@ public class RoomList extends VerticalLayout implements RefreshableTable { var button = new Button("Confirm"); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addClickListener(event -> { - if (!room.getIdentifier().equals(inputField.getValue())) { + if (!room.getId().equals(inputField.getValue())) { Notifications.error("Invalid input"); return; } removeRoom(room); - Notifications.success("Room " + room.getIdentifier() + " was successfully removed!"); + Notifications.success("Room " + room.getId() + " was successfully removed!"); dialog.close(); }); @@ -174,31 +178,23 @@ public class RoomList extends VerticalLayout implements RefreshableTable { dialog.open(); } - public void removeRoom(ShortResponseRoom room) { - roomRepository.removeRoom(room); + public void removeRoom(RoomDTO room) { + client.getRoomRepository().deleteRoom(InstanceId.of(room.getInstanceId()), RoomId.of(room.getId())); refresh(); } - private int getUsers(ShortResponseRoom room) { - var users = room.getDetail(RoomDetails.COUNT_USERS); - if (users == null) return 0; - try { - - return Integer.parseInt(users); - } catch (NumberFormatException ex) { - return 0; - } - } - @Override public void refresh() { - cachedRooms = roomRepository.allRooms(GetAllRooms.withAllDetails(node.getIdentifierObject())); + cachedRooms = client.getRestTemplate().executeGraphQL(AllRooms.query(instance.id().getValue())).getAllRooms() + .stream() + .map(room -> (RoomDTO) room) + .toList(); applySearch(searchField.getValue()); - int users = 0; + int entities = 0; for (var room: cachedRooms) { - users += getUsers(room); + entities += room.getCountEntities(); } - totalUsers.setText("Total users: " + users); + totalEntities.setText("Total entities: " + entities); } } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/UserList.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/component/UserList.java deleted file mode 100644 index 7af1874..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/UserList.java +++ /dev/null @@ -1,130 +0,0 @@ -package ru.dragonestia.picker.cp.component; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.grid.ColumnTextAlign; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import ru.dragonestia.picker.api.model.room.IRoom; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.model.user.UserDetails; -import ru.dragonestia.picker.api.repository.EntityRepository; -import ru.dragonestia.picker.api.repository.query.user.GetAllUsersFromRoom; -import ru.dragonestia.picker.api.repository.query.user.UnlinkUsersFromRoom; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -public class UserList extends VerticalLayout implements RefreshableTable { - - private final IRoom room; - private final EntityRepository entityRepository; - private final Button buttonRemove; - private final Grid usersGrid; - private final Span totalUsers = new Span(); - private final Span occupancy = new Span(); - private List cachedUsers = new ArrayList<>(); - - public UserList(IRoom room, EntityRepository entityRepository) { - this.room = room; - this.entityRepository = entityRepository; - - buttonRemove = createButtonRemove(); - add(usersGrid = createUsersGrid()); - - refresh(); - updateButtonRemove(); - } - - private Button createButtonRemove() { - var button = new Button("Unlink"); - button.setPrefixComponent(new Icon(VaadinIcon.UNLINK)); - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); - button.addClickListener(event -> { - var users = usersGrid.getSelectedItems(); - if (users.isEmpty()) return; - entityRepository.unlinkUsersFromRoom(UnlinkUsersFromRoom.builder() - .setNodeId(room.getNodeIdentifierObject()) - .setRoomId(room.getIdentifierObject()) - .setUsers(users.stream().map(IUser::getIdentifierObject).collect(Collectors.toSet())) - .build()); - refresh(); - }); - return button; - } - - private Grid createUsersGrid() { - var grid = new Grid(); - - grid.addColumn(IUser::getIdentifier).setHeader("User Identifier").setSortable(true).setFooter(totalUsers); - - grid.addColumn(user -> user.getDetail(UserDetails.COUNT_ROOMS)).setTextAlign(ColumnTextAlign.CENTER) - .setHeader("Linked with rooms").setComparator((user1, user2) -> { - var r1 = Integer.parseInt(Objects.requireNonNull(user1.getDetail(UserDetails.COUNT_ROOMS))); - var r2 = Integer.parseInt(Objects.requireNonNull(user2.getDetail(UserDetails.COUNT_ROOMS))); - - return Integer.compare(r1, r2); - }).setSortable(true).setFooter(occupancy); - - grid.addComponentColumn(this::createManageButton).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true) - .setTextAlign(ColumnTextAlign.END).setHeader(createManageTableButtons()); - - grid.setSelectionMode(Grid.SelectionMode.MULTI); - grid.addSelectionListener(event -> updateButtonRemove()); - grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); - return grid; - } - - private Button createManageButton(IUser user) { - var button = new Button("Details"); - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - button.addClickListener(e -> { - getUI().ifPresent(ui -> ui.navigate("/users/" + user.getIdentifier())); - }); - return button; - } - - private HorizontalLayout createManageTableButtons() { - var layout = new HorizontalLayout(); - layout.setJustifyContentMode(JustifyContentMode.END); - - layout.add(buttonRemove); - layout.add(createRefreshButton()); - - return layout; - } - - private void updateButtonRemove() { - var users = usersGrid.getSelectedItems(); - - if (users.isEmpty()) { - buttonRemove.setEnabled(false); - buttonRemove.setText("Unlink"); - return; - } - - buttonRemove.setEnabled(true); - buttonRemove.setText("Unlink(" + users.size() + ")"); - } - - public static int getUsingPercentage(int slots, int usedSlots) { - if (slots == IRoom.UNLIMITED_SLOTS) return -1; - double percent = usedSlots / (double) slots * 100; - return (int) percent; - } - - @Override - public void refresh() { - cachedUsers = entityRepository.getAllUsersFormRoom(GetAllUsersFromRoom.withAllDetails(room.getNodeIdentifierObject(), room.getIdentifierObject())) - .stream().map(user -> (IUser) user).toList(); - usersGrid.setItems(cachedUsers); - totalUsers.setText("Total users: " + cachedUsers.size()); - occupancy.setText("Occupancy: %s".formatted(getUsingPercentage(room.getMaxSlots(), cachedUsers.size()) + "%")); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RestApiConfig.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RestApiConfig.java deleted file mode 100644 index 6e233ab..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RestApiConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.dragonestia.picker.cp.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -import java.util.function.Supplier; - -@Configuration -public class RestApiConfig { - - @Bean - RestTemplateBuilder restTemplateBuilder() { - return new RestTemplateBuilder(); - } - - @Bean - Supplier restTemplateSupplier(@Autowired RestTemplateBuilder builder) { - return builder::build; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java index 94d486e..243f31e 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/RoomPickerConfig.java @@ -3,11 +3,7 @@ package ru.dragonestia.picker.cp.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import ru.dragonestia.picker.api.impl.RoomPickerClient; import ru.dragonestia.picker.cp.annotation.ServerURL; -import ru.dragonestia.picker.cp.model.Account; -import ru.dragonestia.picker.cp.model.provider.AccountProvider; -import ru.dragonestia.picker.cp.util.AdminRoomPickerClient; @Configuration public class RoomPickerConfig { @@ -15,25 +11,9 @@ public class RoomPickerConfig { @Value("${ROOMPICKER_HOST_URL:http://localhost:8080}") private String serverUrl; - @Value("${ROOMPICKER_ADMIN_USERNAME:admin}") - private String adminUsername; - - @Value("${ROOMPICKER_ADMIN_PASSWORD:qwerty123}") - private String adminPassword; - @ServerURL @Bean String severUrl() { return serverUrl; } - - @Bean - RoomPickerClient adminClient() { - return new AdminRoomPickerClient(serverUrl, adminUsername, adminPassword); - } - - @Bean - AccountProvider accountProvider() { - return response -> new Account(response, new RoomPickerClient(serverUrl, response.getUsername(), response.getPassword())); - } } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/SecurityConfig.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/config/SecurityConfig.java deleted file mode 100644 index 6011e50..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/config/SecurityConfig.java +++ /dev/null @@ -1,56 +0,0 @@ -package ru.dragonestia.picker.cp.config; - -import com.vaadin.flow.spring.security.VaadinWebSecurity; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import ru.dragonestia.picker.cp.page.LoginPage; -import ru.dragonestia.picker.cp.service.AccountService; - -@EnableWebSecurity -@Configuration -public class SecurityConfig extends VaadinWebSecurity { - - private AccountService accountService; - - @Autowired - public void setAccountService(AccountService accountService) { - this.accountService = accountService; - } - - @Bean - PasswordEncoder passwordEncoder() { - return new PasswordEncoder() { - @Override - public String encode(CharSequence rawPassword) { - return rawPassword.toString(); - } - - @Override - public boolean matches(CharSequence rawPassword, String encodedPassword) { - return rawPassword.toString().equals(encodedPassword); - } - }; - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(auth -> { - auth.requestMatchers(AntPathRequestMatcher.antMatcher("/static/**")).permitAll(); - }); - - http.userDetailsService(accountService); - - super.configure(http); - - http.formLogin(login -> { - login.successForwardUrl("/instances"); - login.defaultSuccessUrl("/instances"); - }); - setLoginView(http, LoginPage.class); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/error/ApplicationErrorHandler.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/error/ApplicationErrorHandler.java index b131c5d..f08345a 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/error/ApplicationErrorHandler.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/error/ApplicationErrorHandler.java @@ -7,7 +7,7 @@ import com.vaadin.flow.server.ErrorHandler; import lombok.extern.log4j.Log4j2; import ru.dragonestia.picker.api.exception.ApiException; import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions; -import ru.dragonestia.picker.cp.component.Notifications; +import ru.dragonestia.picker.cp.util.Notifications; import java.security.InvalidParameterException; @@ -21,30 +21,22 @@ public class ApplicationErrorHandler implements ErrorHandler { return; } - if (errorEvent.getThrowable() instanceof ApiException ex) { - execute(() -> { - Notifications.error(ex.getMessage()); - }); + if (errorEvent.getThrowable().getClass().getAnnotation(ApiException.class) != null) { + execute(() -> Notifications.error(errorEvent.getThrowable().getMessage())); return; } if (errorEvent.getThrowable() instanceof InvalidParameterException ex) { - execute(() -> { - Notifications.error(ex.getMessage()); - }); + execute(() -> Notifications.error(ex.getMessage())); return; } if (errorEvent.getThrowable() instanceof NotEnoughPermissions) { - execute(() -> { - Notifications.error("Not enough permissions to this action"); - }); + execute(() -> Notifications.error("Not enough permissions to this action")); return; } - execute(() -> { - Notifications.error("Internal server error"); - }); + execute(() -> Notifications.error("Internal server error")); log.throwing(errorEvent.getThrowable()); } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/exception/Unauthorized.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/exception/Unauthorized.java new file mode 100644 index 0000000..4977fca --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/exception/Unauthorized.java @@ -0,0 +1,3 @@ +package ru.dragonestia.picker.cp.exception; + +public class Unauthorized extends RuntimeException {} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/listener/VaadinEventListener.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/listener/VaadinEventListener.java index d60e003..9173568 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/listener/VaadinEventListener.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/listener/VaadinEventListener.java @@ -3,10 +3,8 @@ package ru.dragonestia.picker.cp.listener; import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.VaadinServiceInitListener; import com.vaadin.flow.spring.annotation.SpringComponent; -import lombok.extern.log4j.Log4j2; import ru.dragonestia.picker.cp.error.ApplicationErrorHandler; -@Log4j2 @SpringComponent public class VaadinEventListener implements VaadinServiceInitListener { diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Account.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Account.java deleted file mode 100644 index 6a9fd9b..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Account.java +++ /dev/null @@ -1,74 +0,0 @@ -package ru.dragonestia.picker.cp.model; - -import org.jetbrains.annotations.NotNull; -import org.springframework.security.core.userdetails.UserDetails; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.account.IAccount; -import ru.dragonestia.picker.api.model.account.ResponseAccount; - -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; - -public class Account implements IAccount, UserDetails { - - private final ResponseAccount original; - private final RoomPickerClient client; - private final Set permissions; - - public Account(ResponseAccount original, RoomPickerClient client) { - this.original = original; - this.client = client; - permissions = original.getPermissions().stream().map(permission -> new Permission("ROLE_" + permission)).collect(Collectors.toSet()); - permissions.add(new Permission("ROLE_USER")); - } - - public @NotNull RoomPickerClient getClient() { - return client; - } - - @Override - public Collection getAuthorities() { - return permissions; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !original.isLocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - - @Override - public @NotNull String getUsername() { - return original.getUsername(); - } - - @Override - public @NotNull String getPassword() { - return original.getPassword(); - } - - @Override - public @NotNull Set getPermissions() { - return original.getPermissions(); - } - - @Override - public boolean isLocked() { - return original.isLocked(); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/AccountSession.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/model/AccountSession.java new file mode 100644 index 0000000..b2ed224 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/model/AccountSession.java @@ -0,0 +1,21 @@ +package ru.dragonestia.picker.cp.model; + +import lombok.Getter; +import lombok.Setter; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.account.Account; + +@Setter +@Getter +public class AccountSession { + + private final Account data; + private String password; + private final RoomPickerClient client; + + public AccountSession(Account data, String password, RoomPickerClient client) { + this.data = data; + this.password = password; + this.client = client; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Permission.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Permission.java deleted file mode 100644 index e636e92..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/Permission.java +++ /dev/null @@ -1,34 +0,0 @@ -package ru.dragonestia.picker.cp.model; - -import com.github.javaparser.quality.NotNull; -import lombok.Getter; -import org.springframework.security.core.GrantedAuthority; - -public class Permission implements GrantedAuthority { - - private final String authority; - - public Permission(@NotNull String authority) { - this.authority = authority; - } - - @Override - public String getAuthority() { - return authority; - } - - @Getter - public enum Enum { - // All from ru.dragonestia.picker.model.Permission (server) - // Except for USER and ADMIN - - NODE_MANAGEMENT("Create and remove nodes"), - ; - - private final String description; - - Enum(String description) { - this.description = description; - } - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/provider/AccountProvider.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/model/provider/AccountProvider.java deleted file mode 100644 index 1e6fd60..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/model/provider/AccountProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.dragonestia.picker.cp.model.provider; - -import org.jetbrains.annotations.NotNull; -import ru.dragonestia.picker.api.model.account.ResponseAccount; -import ru.dragonestia.picker.cp.model.Account; - -public interface AccountProvider { - - @NotNull Account provide(@NotNull ResponseAccount responseAccount); -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountsPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountsPage.java deleted file mode 100644 index 9e7ef2a..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountsPage.java +++ /dev/null @@ -1,29 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.security.RolesAllowed; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; -import ru.dragonestia.picker.api.repository.AccountRepository; -import ru.dragonestia.picker.cp.component.AccountList; -import ru.dragonestia.picker.cp.service.SecurityService; - -@RolesAllowed("ADMIN") -@PageTitle("Accounts") -@Route(value = "/admin/accounts", layout = MainLayout.class) -@RequiredArgsConstructor -public class AccountsPage extends VerticalLayout { - - private final AccountRepository accountRepository; - - @Autowired - public AccountsPage(SecurityService securityService) { - accountRepository = securityService.getAuthenticatedAccount().getClient().getAccountRepository(); - - add(new H2("Account management")); - add(new AccountList(accountRepository)); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/LoginPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/LoginPage.java deleted file mode 100644 index 1b7648b..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/LoginPage.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.Html; -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.login.LoginForm; -import com.vaadin.flow.component.login.LoginI18n; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.*; -import jakarta.annotation.security.PermitAll; -import lombok.extern.log4j.Log4j2; -import ru.dragonestia.picker.cp.service.SecurityService; - -@Log4j2 -@PermitAll -@Route("/login") -public class LoginPage extends VerticalLayout implements BeforeEnterObserver, AfterNavigationObserver { - - private final LoginForm formLogin; - private final boolean authenticated; - - public LoginPage(SecurityService securityService) { - if (securityService.getAuthenticatedAccount() != null) { - formLogin = null; - authenticated = true; - return; - } - - authenticated = false; - - setAlignItems(Alignment.CENTER); - - add(new Html("

RoomPicker!

")); - add(formLogin = createFormLogin()); - } - - private LoginForm createFormLogin() { - var form = new LoginForm(); - form.setAction("login"); - form.setForgotPasswordButtonVisible(false); - - var i18n = LoginI18n.createDefault(); - i18n.getForm().setTitle(null); - i18n.getForm().setUsername("Account username"); - i18n.getForm().setSubmit("Login"); - form.setI18n(i18n); - return form; - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - if(event.getLocation() - .getQueryParameters() - .getParameters() - .containsKey("error")) { - - formLogin.setError(true); - } - } - - @Override - public void afterNavigation(AfterNavigationEvent afterNavigationEvent) { - if (!authenticated) return; - - getUI().ifPresent(ui -> ui.navigate("/nodes")); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodeDetailsPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodeDetailsPage.java deleted file mode 100644 index c09a699..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodeDetailsPage.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.Html; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Hr; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.security.RolesAllowed; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.cp.component.RoomList; -import ru.dragonestia.picker.cp.component.NavPath; -import ru.dragonestia.picker.cp.component.RegisterRoom; -import ru.dragonestia.picker.cp.service.SecurityService; -import ru.dragonestia.picker.cp.util.RouteParamsExtractor; - -@Getter -@RequiredArgsConstructor -@RolesAllowed("USER") -@PageTitle("Rooms") -@Route(value = "/instances/:nodeId", layout = MainLayout.class) -public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver { - - private final SecurityService securityService; - private final RouteParamsExtractor paramsExtractor; - - private RoomPickerClient client; - private INode node; - private RegisterRoom registerRoom; - private RoomList roomList; - - @PostConstruct - void postConstruct() { - client = securityService.getAuthenticatedAccount().getClient(); - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - node = paramsExtractor.extractNode(event); - - initComponents(node); - } - - private void initComponents(INode node) { - add(NavPath.toNode(node.getIdentifier())); - printNodeDetails(node); - add(new Hr()); - add(registerRoom = new RegisterRoom(node, roomDefinition -> { - try { - client.getRoomRepository().saveRoom(roomDefinition); - return new RegisterRoom.Response(false, null); - } catch (Error error) { - return new RegisterRoom.Response(true, error.getMessage()); - } finally { - roomList.refresh(); - } - })); - add(new Hr()); - add(roomList = new RoomList(node, client.getRoomRepository())); - } - - private void printNodeDetails(INode node) { - add(new H2("Node details")); - - var layout = new VerticalLayout(); - layout.add(new Html("Identifier: " + node.getIdentifier() + "")); - layout.add(new Html("Mode: " + node.getPickingMethod().name() + "")); - - add(layout); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodesPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodesPage.java deleted file mode 100644 index cdaa08b..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/NodesPage.java +++ /dev/null @@ -1,56 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.html.Hr; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteAlias; -import jakarta.annotation.security.RolesAllowed; -import org.springframework.beans.factory.annotation.Autowired; -import ru.dragonestia.picker.api.exception.ApiException; -import ru.dragonestia.picker.api.repository.InstanceRepository; -import ru.dragonestia.picker.cp.component.NavPath; -import ru.dragonestia.picker.cp.component.NodeList; -import ru.dragonestia.picker.cp.component.RegisterNode; -import ru.dragonestia.picker.cp.service.SecurityService; - -@RolesAllowed("USER") -@PageTitle("Nodes") -@RouteAlias(value = "/", layout = MainLayout.class) -@Route(value = "/instances", layout = MainLayout.class) -public class NodesPage extends VerticalLayout { - - private final InstanceRepository instanceRepository; - private final NodeList nodeList; - - @Autowired - public NodesPage(SecurityService securityService) { - this.instanceRepository = securityService.getAuthenticatedAccount().getClient().getNodeRepository(); - - add(NavPath.rootNodes()); - - if (securityService.hasRole("NODE_MANAGEMENT")) { - add(createRegisterNodeElement()); - } - - add(new Hr()); - add(nodeList = createNodeListElement()); - } - - protected RegisterNode createRegisterNodeElement() { - return new RegisterNode(nodeDefinition -> { - try { - instanceRepository.saveNode(nodeDefinition); - return new RegisterNode.Response(false, ""); - } catch (ApiException ex) { - return new RegisterNode.Response(true, ex.getMessage()); - } finally { - nodeList.refresh(); - } - }); - } - - protected NodeList createNodeListElement() { - return new NodeList(instanceRepository); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/RoomDetailsPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/RoomDetailsPage.java deleted file mode 100644 index 5c29473..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/RoomDetailsPage.java +++ /dev/null @@ -1,151 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.Html; -import com.vaadin.flow.component.Unit; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Hr; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.TextArea; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.security.RolesAllowed; -import lombok.RequiredArgsConstructor; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.api.model.room.IRoom; -import ru.dragonestia.picker.api.model.room.ResponseRoom; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.repository.query.user.LinkUsersWithRoom; -import ru.dragonestia.picker.cp.component.AddUsers; -import ru.dragonestia.picker.cp.component.NavPath; -import ru.dragonestia.picker.cp.component.Notifications; -import ru.dragonestia.picker.cp.component.UserList; -import ru.dragonestia.picker.cp.service.SecurityService; -import ru.dragonestia.picker.cp.util.RouteParamsExtractor; - -import java.util.Collection; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -@RequiredArgsConstructor -@RolesAllowed("USER") -@PageTitle("Room details") -@Route(value = "/instances/:nodeId/rooms/:roomId", layout = MainLayout.class) -public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserver { - - private final SecurityService securityService; - private final RouteParamsExtractor paramsExtractor; - - private RoomPickerClient client; - private INode node; - private ResponseRoom room; - private UserList userList; - private Button lockRoomButton; - private VerticalLayout roomInfo; - - @PostConstruct - void postConstruct() { - client = securityService.getAuthenticatedAccount().getClient(); - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - node = paramsExtractor.extractNode(event); - room = (ResponseRoom) paramsExtractor.extractRoom(event, node); - - init(); - } - - private void init() { - add(NavPath.toRoom(node.getIdentifier(), room.getIdentifier())); - add(new H2("Room details")); - printRoomDetails(); - add(new Hr()); - add(new AddUsers(room, (users, ignoreLimitation) -> appendUsers(room, users, ignoreLimitation))); - add(new Hr()); - add(new H2("Users")); - add(userList = new UserList(room, client.getUserRepository())); - } - - private void updateRoomInfo() { - roomInfo.removeAll(); - roomInfo.add(new Html("Node identifier: " + room.getInstanceIdentifier() + "")); - roomInfo.add(new Html("Room identifier: " + room.getIdentifier() + "")); - roomInfo.add(new Html("Slots: " + (room.hasUnlimitedSlots()? "Unlimited" : room.getMaxSlots()) + "")); - roomInfo.add(new Html("Locked: " + (room.isLocked()? "Yes" : "No") + "")); - } - - private void printRoomDetails() { - add(roomInfo = new VerticalLayout()); - roomInfo.setPadding(false); - - updateRoomInfo(); - add(lockRoomButton = new Button("", event -> changeBucketLockedState())); - setLockRoomButtonState(); - - var payload = new TextArea("Payload(" + room.getPayload().length() + ")"); - payload.setValue(room.getPayload()); - payload.setReadOnly(true); - payload.setMinWidth(50, Unit.REM); - add(payload); - } - - private void setLockRoomButtonState() { - if (room.isLocked()) { - lockRoomButton.setText("Unlock"); - lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.UNLOCK)); - } else { - lockRoomButton.setText("Lock"); - lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.LOCK)); - } - } - - private void changeBucketLockedState() { - var newValue = !room.isLocked(); - client.getRoomRepository().lockRoom(room.getPath(), newValue); - - room.setLocked(newValue); - setLockRoomButtonState(); - updateRoomInfo(); - - Notifications.success("Success"); - } - - private void appendUsers(IRoom room, Collection users, boolean ignoreLimitation) { - AtomicBoolean validationFail = new AtomicBoolean(false); - - var newUsers = users.stream() - .filter(user -> { - if (user.getIdentifier().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) { - return true; - } - - validationFail.set(true); - return false; - }).toList(); - - client.getUserRepository().linkUsersWithRoom(LinkUsersWithRoom.builder() - .setNodeId(room.getNodeIdentifierObject()) - .setRoomId(room.getIdentifierObject()) - .setUsers(users.stream().map(IUser::getIdentifierObject).collect(Collectors.toSet())) - .setIgnoreSlotLimitation(ignoreLimitation) - .build()); - userList.refresh(); - - if (validationFail.get()) { - if (newUsers.isEmpty()) { - Notifications.error("All users entered were added because they do not comply with the rule for writing the user identifier"); - } else { - Notifications.warn("Not all users entered were added because they do not comply with the rule for writing the user identifier"); - } - } else { - Notifications.success("Success"); - } - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserDetailsPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserDetailsPage.java deleted file mode 100644 index 785eb4e..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserDetailsPage.java +++ /dev/null @@ -1,96 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.grid.ColumnTextAlign; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.H3; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.security.RolesAllowed; -import lombok.RequiredArgsConstructor; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.room.RoomDetails; -import ru.dragonestia.picker.api.model.room.ShortResponseRoom; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.repository.query.user.FindRoomsLinkedWithUser; -import ru.dragonestia.picker.cp.component.RefreshableTable; -import ru.dragonestia.picker.cp.service.SecurityService; -import ru.dragonestia.picker.cp.util.RouteParamsExtractor; - -import java.util.List; -import java.util.Objects; - -@RequiredArgsConstructor -@RolesAllowed("USER") -@PageTitle("User details") -@Route(value = "/users/:userId", layout = MainLayout.class) -public class UserDetailsPage extends VerticalLayout implements BeforeEnterObserver, RefreshableTable { - - private final SecurityService securityService; - private final RouteParamsExtractor paramsExtractor; - - private RoomPickerClient client; - private IUser user; - private Grid gridRooms; - - @PostConstruct - void postConstruct() { - client = securityService.getAuthenticatedAccount().getClient(); - } - - @Override - public void beforeEnter(BeforeEnterEvent event) { - user = paramsExtractor.extractUser(event); - - init(); - } - - private void init() { - add(new H2("User '%s'".formatted(user.getIdentifier()))); - add(new H3("Linked with rooms")); - add(gridRooms = createGrid()); - - refresh(); - } - - private Grid createGrid() { - var grid = new Grid(); - - grid.addColumn(ShortResponseRoom::getIdentifier).setHeader("Room identifier").setSortable(true); - - grid.addColumn(ShortResponseRoom::getInstanceIdentifier).setHeader("Node identifier").setSortable(true); - - grid.addColumn(room -> room.getDetail(RoomDetails.COUNT_USERS)).setHeader("Users") - .setComparator((room1, room2) -> { - var r1 = Integer.parseInt(Objects.requireNonNull(room1.getDetail(RoomDetails.COUNT_USERS))); - var r2 = Integer.parseInt(Objects.requireNonNull(room2.getDetail(RoomDetails.COUNT_USERS))); - - return Integer.compare(r1, r2); - }).setSortable(true); - - grid.addComponentColumn(room -> { - var button = new Button("Details"); - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - button.addClickListener(event -> { - getUI().ifPresent(ui -> ui.navigate("/nodes/%s/rooms/%s".formatted(room.getInstanceIdentifier(), room.getIdentifier()))); - }); - return button; - }).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true).setHeader(createRefreshButton()); - - grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); - return grid; - } - - @Override - public void refresh() { - List cachedRooms = client.getUserRepository() - .findRoomsLinkedWithUser(FindRoomsLinkedWithUser.withAllDetails(user.getIdentifierObject())); - gridRooms.setItems(cachedRooms); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserSearchPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserSearchPage.java deleted file mode 100644 index 9c2be54..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/UserSearchPage.java +++ /dev/null @@ -1,115 +0,0 @@ -package ru.dragonestia.picker.cp.page; - -import com.vaadin.flow.component.Key; -import com.vaadin.flow.component.Unit; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.grid.ColumnTextAlign; -import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.security.RolesAllowed; -import org.springframework.beans.factory.annotation.Autowired; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.model.user.UserDetails; -import ru.dragonestia.picker.api.repository.EntityRepository; -import ru.dragonestia.picker.api.repository.query.user.SearchUsers; -import ru.dragonestia.picker.api.repository.type.EntityIdentifier; -import ru.dragonestia.picker.cp.component.RefreshableTable; -import ru.dragonestia.picker.cp.service.SecurityService; - -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -@RolesAllowed("USER") -@PageTitle("Search users") -@Route(value = "/users", layout = MainLayout.class) -public class UserSearchPage extends VerticalLayout implements RefreshableTable { - - private final EntityRepository entityRepository; - private final TextField fieldUsername; - private final Grid userGrid; - private final Span foundUsers; - private List cachedUsers = new LinkedList<>(); - - @Autowired - public UserSearchPage(SecurityService securityService) { - this.entityRepository = securityService.getAuthenticatedAccount().getClient().getUserRepository(); - - foundUsers = new Span(); - add(fieldUsername = createUsernameInputField()); - add(userGrid = createUserGrid()); - justRefresh(); - } - - private TextField createUsernameInputField() { - var field = new TextField(); - field.setLabel("Username"); - field.setPlaceholder("some-user-identifier"); - field.setRequired(true); - field.setMinWidth(30, Unit.PERCENTAGE); - field.setAutofocus(true); - - var button = new Button(new Icon(VaadinIcon.SEARCH)); - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - button.getStyle().set("color", "#FFFFFF"); - button.addClickListener(event -> refresh()); - button.addClickShortcut(Key.ENTER); - - field.setSuffixComponent(button); - return field; - } - - private Grid createUserGrid() { - var grid = new Grid(); - - grid.addColumn(IUser::getIdentifier).setHeader("Identifier").setSortable(true) - .setFooter(foundUsers); - - grid.addColumn(user -> user.getDetail(UserDetails.COUNT_ROOMS)).setComparator((user1, user2) -> { - var r1 = Integer.parseInt(Objects.requireNonNull(user1.getDetail(UserDetails.COUNT_ROOMS))); - var r2 = Integer.parseInt(Objects.requireNonNull(user2.getDetail(UserDetails.COUNT_ROOMS))); - - return Integer.compare(r1, r2); - }).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with rooms"); - - grid.addComponentColumn(user -> { - var button = new Button("Details"); - button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - button.addClickListener(event -> { - getUI().ifPresent(ui -> ui.navigate("/users/" + user.getIdentifier())); - }); - return button; - }).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true).setHeader(createRefreshButton()); - - grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); - return grid; - } - - private void search(String input) { - System.out.println("Input: " + input); - if (input.isEmpty()) { - userGrid.setItems(); - } - - userGrid.setItems(cachedUsers = entityRepository.searchUsers(SearchUsers.withAllDetails(EntityIdentifier.of(input))) - .stream().map(user -> (IUser) user).toList()); - } - - @Override - public void refresh() { - search(fieldUsername.getValue().trim()); - foundUsers.setText("Found %s users".formatted(cachedUsers.size())); - } - - public void justRefresh() { - userGrid.setItems(); - foundUsers.setText("Found %s users".formatted(cachedUsers.size())); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidNodeIdentifierPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidNodeIdentifierPlug.java deleted file mode 100644 index 01019bd..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidNodeIdentifierPlug.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.dragonestia.picker.cp.page.plug; - -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.ErrorParameter; -import com.vaadin.flow.router.HasErrorParameter; -import jakarta.servlet.http.HttpServletResponse; -import ru.dragonestia.picker.api.exception.InvalidInstanceIdentifierException; -import ru.dragonestia.picker.cp.component.NavPath; - -public class InvalidNodeIdentifierPlug extends ErrorPlug implements HasErrorParameter { - - @Override - public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter errorParameter) { - var ex = errorParameter.getException(); - var nodeId = ex.getNodeId(); - - init(NavPath.toNode(nodeId), "Error 400", ex.getMessage()); - return HttpServletResponse.SC_NOT_FOUND; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidRoomIdentifierPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidRoomIdentifierPlug.java deleted file mode 100644 index f0d929b..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/InvalidRoomIdentifierPlug.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.dragonestia.picker.cp.page.plug; - -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.ErrorParameter; -import com.vaadin.flow.router.HasErrorParameter; -import jakarta.servlet.http.HttpServletResponse; -import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException; -import ru.dragonestia.picker.cp.component.NavPath; - -public class InvalidRoomIdentifierPlug extends ErrorPlug implements HasErrorParameter { - - @Override - public int setErrorParameter(BeforeEnterEvent event, ErrorParameter errorParameter) { - var ex = errorParameter.getException(); - var nodeId = ex.getNodeId(); - var roomId = ex.getRoomId(); - - init(NavPath.toRoom(nodeId, roomId), "Error 400", ex.getMessage()); - return HttpServletResponse.SC_NOT_FOUND; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NodeNotFoundPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NodeNotFoundPlug.java deleted file mode 100644 index 2aa6150..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NodeNotFoundPlug.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.dragonestia.picker.cp.page.plug; - -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.ErrorParameter; -import com.vaadin.flow.router.HasErrorParameter; -import jakarta.servlet.http.HttpServletResponse; -import ru.dragonestia.picker.api.exception.InstanceNotFoundException; -import ru.dragonestia.picker.cp.component.NavPath; - -public class NodeNotFoundPlug extends ErrorPlug implements HasErrorParameter { - - @Override - public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter errorParameter) { - var ex = errorParameter.getException(); - var nodeId = ex.getNodeId(); - - init(NavPath.toNode(nodeId), "Error 404", ex.getMessage()); - return HttpServletResponse.SC_NOT_FOUND; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/RoomNotFoundPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/RoomNotFoundPlug.java deleted file mode 100644 index 3b56b93..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/RoomNotFoundPlug.java +++ /dev/null @@ -1,21 +0,0 @@ -package ru.dragonestia.picker.cp.page.plug; - -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.ErrorParameter; -import com.vaadin.flow.router.HasErrorParameter; -import jakarta.servlet.http.HttpServletResponse; -import ru.dragonestia.picker.api.exception.RoomNotFoundException; -import ru.dragonestia.picker.cp.component.NavPath; - -public class RoomNotFoundPlug extends ErrorPlug implements HasErrorParameter { - - @Override - public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter errorParameter) { - var ex = errorParameter.getException(); - var nodeId = ex.getNodeId(); - var roomId = ex.getRoomId(); - - init(NavPath.toRoom(nodeId, roomId), "Error 404", ex.getMessage()); - return HttpServletResponse.SC_NOT_FOUND; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/InstanceRepository.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/InstanceRepository.java new file mode 100644 index 0000000..869fd8e --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/InstanceRepository.java @@ -0,0 +1,10 @@ +package ru.dragonestia.picker.cp.repository; + +import ru.dragonestia.picker.cp.repository.dto.InstanceDTO; + +import java.util.List; + +public interface InstanceRepository { + + List all(); +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/EntityDTO.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/EntityDTO.java new file mode 100644 index 0000000..9da593a --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/EntityDTO.java @@ -0,0 +1,8 @@ +package ru.dragonestia.picker.cp.repository.dto; + +public interface EntityDTO { + + String getId(); + + int getCountRooms(); +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/InstanceDTO.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/InstanceDTO.java new file mode 100644 index 0000000..af49947 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/InstanceDTO.java @@ -0,0 +1,14 @@ +package ru.dragonestia.picker.cp.repository.dto; + +import ru.dragonestia.picker.api.model.instance.type.PickingMethod; + +public interface InstanceDTO { + + String getId(); + + PickingMethod getMethod(); + + boolean isPersist(); + + int getCountRooms(); +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/RoomDTO.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/RoomDTO.java new file mode 100644 index 0000000..851ea22 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/dto/RoomDTO.java @@ -0,0 +1,16 @@ +package ru.dragonestia.picker.cp.repository.dto; + +public interface RoomDTO { + + String getId(); + + String getInstanceId(); + + int getSlots(); + + boolean isLocked(); + + int getCountEntities(); + + boolean isPersist(); +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllEntities.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllEntities.java new file mode 100644 index 0000000..b484d47 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllEntities.java @@ -0,0 +1,42 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; + +import java.util.List; + +@Getter +public class AllEntities { + + private final static String QUERY = """ + query ($instanceId: String!, $roomId: String!) { + roomById(nodeId: $instanceId, roomId: $roomId) { + entities { + id + countRooms + } + } + } + """; + + private Room roomById; + + public static GraphqlQuery query(String instanceId, String roomId) { + return new GraphqlQuery<>(QUERY, AllEntities.class, params -> { + params.put("instanceId", instanceId); + params.put("roomId", roomId); + }); + } + + @Getter + public static class Room { + private List entities; + } + + @Getter + public static class Entity implements EntityDTO { + private String id; + private int countRooms; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllInstances.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllInstances.java new file mode 100644 index 0000000..e4d4445 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllInstances.java @@ -0,0 +1,37 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.api.model.instance.type.PickingMethod; +import ru.dragonestia.picker.cp.repository.dto.InstanceDTO; + +import java.util.List; + +@Getter +public class AllInstances { + + private final static String QUERY = """ + { + allInstances { + id + method + persist + countRooms + } + } + """; + + private List allInstances; + + public static GraphqlQuery query() { + return new GraphqlQuery<>(QUERY, AllInstances.class, params -> {}); + } + + @Getter + public static class Instance implements InstanceDTO { + private String id; + private PickingMethod method; + private boolean persist; + private int countRooms; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllRooms.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllRooms.java new file mode 100644 index 0000000..b2d26c8 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/AllRooms.java @@ -0,0 +1,42 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.cp.repository.dto.RoomDTO; + +import java.util.List; + +@Getter +public class AllRooms { + + private final static String QUERY = """ + query ($nodeId: String!) { + allRooms(nodeId: $nodeId) { + id + instanceId + slots + locked + countEntities + persist + } + } + """; + + private List allRooms; + + public static GraphqlQuery query(String nodeId) { + return new GraphqlQuery<>(QUERY, AllRooms.class, params -> { + params.put("nodeId", nodeId); + }); + } + + @Getter + public static class Room implements RoomDTO { + private String id; + private String instanceId; + private int slots; + private boolean locked; + private int countEntities; + private boolean persist; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityData.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityData.java new file mode 100644 index 0000000..f9b017e --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityData.java @@ -0,0 +1,32 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; + +@Getter +public class EntityData { + + private final static String QUERY = """ + query ($entityId: String!) { + entityById(id: $entityId) { + id + countRooms + } + } + """; + + private Entity entityById; + + public static GraphqlQuery query(String entityId) { + return new GraphqlQuery<>(QUERY, EntityData.class, params -> { + params.put("entityId", entityId); + }); + } + + @Getter + public static class Entity implements EntityDTO { + private String id; + private int countRooms; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityRooms.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityRooms.java new file mode 100644 index 0000000..c4d2070 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/EntityRooms.java @@ -0,0 +1,49 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.cp.repository.dto.RoomDTO; + +import java.util.List; + +@Getter +public class EntityRooms { + + private final static String QUERY = """ + query ($entityId: String!) { + entityById(id: $entityId) { + rooms { + id + instanceId + countEntities + locked + persist + slots + } + } + } + """; + + private Entity entityById; + + public static GraphqlQuery query(String entityId) { + return new GraphqlQuery<>(QUERY, EntityRooms.class, params -> { + params.put("entityId", entityId); + }); + } + + @Getter + public static class Entity { + private List rooms; + } + + @Getter + public static class Room implements RoomDTO { + private String id; + private String instanceId; + private int slots; + private boolean locked; + private int countEntities; + private boolean persist; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/SearchEntity.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/SearchEntity.java new file mode 100644 index 0000000..5185b16 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/graphql/SearchEntity.java @@ -0,0 +1,34 @@ +package ru.dragonestia.picker.cp.repository.graphql; + +import lombok.Getter; +import ru.dragonestia.picker.api.impl.util.GraphqlQuery; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; + +import java.util.List; + +@Getter +public class SearchEntity { + + private final static String QUERY = """ + query ($input: String!) { + searchEntity(input: $input) { + id + countRooms + } + } + """; + + private List searchEntity; + + public static GraphqlQuery query(String input) { + return new GraphqlQuery<>(QUERY, SearchEntity.class, params -> { + params.put("input", input); + }); + } + + @Getter + public static class Entity implements EntityDTO { + private String id; + private int countRooms; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/impl/InstanceRepositoryImpl.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/impl/InstanceRepositoryImpl.java new file mode 100644 index 0000000..e3341eb --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/repository/impl/InstanceRepositoryImpl.java @@ -0,0 +1,22 @@ +package ru.dragonestia.picker.cp.repository.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.dragonestia.picker.cp.repository.InstanceRepository; +import ru.dragonestia.picker.cp.repository.dto.InstanceDTO; +import ru.dragonestia.picker.cp.repository.graphql.AllInstances; +import ru.dragonestia.picker.cp.service.SessionService; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class InstanceRepositoryImpl implements InstanceRepository { + + private final SessionService sessionService; + + @Override + public List all() { + return sessionService.getSession().getClient().getRestTemplate().executeGraphQL(AllInstances.query()).getAllInstances(); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/service/AccountService.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/service/AccountService.java deleted file mode 100644 index 82e1de6..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/service/AccountService.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.dragonestia.picker.cp.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.cp.model.Account; -import ru.dragonestia.picker.cp.model.provider.AccountProvider; - -@Service -@RequiredArgsConstructor -public class AccountService implements UserDetailsService { - - private final RoomPickerClient adminClient; - private final AccountProvider accountProvider; - - @Override - public Account loadUserByUsername(String username) throws UsernameNotFoundException { - var response = adminClient.getAccountRepository().findAccountByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException(username)); - - return accountProvider.provide(response); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SecurityService.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SecurityService.java deleted file mode 100644 index 233152d..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SecurityService.java +++ /dev/null @@ -1,36 +0,0 @@ -package ru.dragonestia.picker.cp.service; - -import com.vaadin.flow.component.UI; -import com.vaadin.flow.server.VaadinServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; -import org.springframework.stereotype.Service; -import ru.dragonestia.picker.cp.model.Account; - -@Service -@RequiredArgsConstructor -public class SecurityService { - - public Account getAuthenticatedAccount() { - var context = SecurityContextHolder.getContext(); - if (context != null && context.getAuthentication().getPrincipal() instanceof Account account) { - return account; - } - return null; - } - - public void logout() { - UI.getCurrent().getPage().setLocation("/login"); - var logoutHandler = new SecurityContextLogoutHandler(); - logoutHandler.logout(VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null); - } - - public boolean hasRole(String role) { - var r = "ROLE_" + role; - for (var permission: getAuthenticatedAccount().getAuthorities()) { - if (r.equals(permission.getAuthority())) return true; - } - return false; - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SessionService.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SessionService.java new file mode 100644 index 0000000..490cfd2 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/service/SessionService.java @@ -0,0 +1,40 @@ +package ru.dragonestia.picker.cp.service; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinSession; +import org.springframework.stereotype.Component; +import ru.dragonestia.picker.api.model.account.Permission; +import ru.dragonestia.picker.cp.exception.Unauthorized; +import ru.dragonestia.picker.cp.model.AccountSession; + +@Component +public class SessionService { + + public void setSession(AccountSession session) { + VaadinSession.getCurrent().setAttribute(AccountSession.class, session); + } + + public AccountSession getSession() { + return VaadinSession.getCurrent().getAttribute(AccountSession.class); + } + + public void checkAuthorisation() throws Unauthorized { + if (VaadinSession.getCurrent().getAttribute(AccountSession.class) == null) { + throw new Unauthorized(); + } + } + + public void login(AccountSession session, UI ui) { + setSession(session); + ui.getPage().setLocation("/"); + } + + public void logout(UI ui) { + VaadinSession.getCurrent().setAttribute(AccountSession.class, null); + ui.getPage().setLocation("/login"); + } + + public boolean hasPermission(Permission permission) { + return getSession().getData().permissions().contains(permission); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/AdminRoomPickerClient.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/AdminRoomPickerClient.java deleted file mode 100644 index 1987b97..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/AdminRoomPickerClient.java +++ /dev/null @@ -1,29 +0,0 @@ -package ru.dragonestia.picker.cp.util; - -import org.jetbrains.annotations.NotNull; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.repository.InstanceRepository; -import ru.dragonestia.picker.api.repository.RoomRepository; -import ru.dragonestia.picker.api.repository.EntityRepository; - -public class AdminRoomPickerClient extends RoomPickerClient { - - public AdminRoomPickerClient(@NotNull String url, @NotNull String username, @NotNull String password) { - super(url, username, password); - } - - @Override - public @NotNull InstanceRepository getNodeRepository() { - throw new UnsupportedOperationException(); - } - - @Override - public @NotNull RoomRepository getRoomRepository() { - throw new UnsupportedOperationException(); - } - - @Override - public @NotNull EntityRepository getUserRepository() { - throw new UnsupportedOperationException(); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/Notifications.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/Notifications.java similarity index 97% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/component/Notifications.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/util/Notifications.java index 8a83c83..04db6da 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/component/Notifications.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/Notifications.java @@ -1,4 +1,4 @@ -package ru.dragonestia.picker.cp.component; +package ru.dragonestia.picker.cp.util; import com.vaadin.flow.component.Html; import com.vaadin.flow.component.button.Button; diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/PermissionDescription.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/PermissionDescription.java new file mode 100644 index 0000000..c6f84df --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/PermissionDescription.java @@ -0,0 +1,23 @@ +package ru.dragonestia.picker.cp.util; + +import com.github.javaparser.quality.NotNull; +import ru.dragonestia.picker.api.model.account.Permission; + +import java.util.HashMap; +import java.util.Map; + +public class PermissionDescription { + + private final static Map map; + + static { + map = new HashMap<>(); + map.put(Permission.NODE_MANAGEMENT, "Create and remove instances"); + } + + private PermissionDescription() {} + + public static String of(@NotNull Permission permission) { + return map.getOrDefault(permission, permission.name()); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamExtractor.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamExtractor.java new file mode 100644 index 0000000..fafed9d --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamExtractor.java @@ -0,0 +1,49 @@ +package ru.dragonestia.picker.cp.util; + +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.RouteParameters; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import ru.dragonestia.picker.api.exception.DoesNotExistsException; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.account.Account; +import ru.dragonestia.picker.api.model.account.AccountId; +import ru.dragonestia.picker.api.model.instance.Instance; +import ru.dragonestia.picker.api.model.instance.InstanceId; +import ru.dragonestia.picker.api.model.room.Room; +import ru.dragonestia.picker.api.model.room.RoomId; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; +import ru.dragonestia.picker.cp.repository.graphql.EntityData; +import ru.dragonestia.picker.cp.service.SessionService; + +@Component +@RequiredArgsConstructor +public class RouteParamExtractor { + + private final SessionService sessionService; + + private RoomPickerClient client() { + return sessionService.getSession().getClient(); + } + + public Instance instance(RouteParameters params) throws DoesNotExistsException { + var id = params.get("instanceId").map(InstanceId::of).orElseThrow(); + return client().getInstanceRepository().getInstance(id); + } + + public Room room(RouteParameters params) throws DoesNotExistsException { + var instanceId = params.get("instanceId").map(InstanceId::of).orElseThrow(); + var roomId = params.get("roomId").map(RoomId::of).orElseThrow(); + return client().getRoomRepository().getRoom(instanceId, roomId); + } + + public EntityDTO entity(RouteParameters params) throws DoesNotExistsException { + var id = params.get("entityId").orElseThrow(); + return client().getRestTemplate().executeGraphQL(EntityData.query(id)).getEntityById(); + } + + public Account account(RouteParameters params) throws DoesNotExistsException { + var id = params.get("accountId").map(AccountId::of).orElseThrow(); + return client().getAccountRepository().getAccount(id); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamsExtractor.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamsExtractor.java deleted file mode 100644 index 47d23cc..0000000 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/RouteParamsExtractor.java +++ /dev/null @@ -1,46 +0,0 @@ -package ru.dragonestia.picker.cp.util; - -import com.vaadin.flow.router.BeforeEnterEvent; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import ru.dragonestia.picker.api.exception.InstanceNotFoundException; -import ru.dragonestia.picker.api.exception.RoomNotFoundException; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.account.IAccount; -import ru.dragonestia.picker.api.model.node.INode; -import ru.dragonestia.picker.api.model.room.IRoom; -import ru.dragonestia.picker.api.model.user.IUser; -import ru.dragonestia.picker.api.repository.query.node.FindNodeById; -import ru.dragonestia.picker.api.repository.query.room.FindRoomById; -import ru.dragonestia.picker.api.repository.query.user.FindUserById; -import ru.dragonestia.picker.api.repository.type.NodeIdentifier; -import ru.dragonestia.picker.api.repository.type.RoomIdentifier; -import ru.dragonestia.picker.api.repository.type.EntityIdentifier; - -@Component -@RequiredArgsConstructor -public class RouteParamsExtractor { - - private final RoomPickerClient client; - - public INode extractNode(BeforeEnterEvent e) throws InstanceNotFoundException { - var nodeId = NodeIdentifier.of(e.getRouteParameters().get("nodeId").orElseThrow(() -> new InstanceNotFoundException("null"))); - return client.getNodeRepository().findNodeById(FindNodeById.justFind(nodeId)).orElseThrow(() -> new InstanceNotFoundException(nodeId.getValue())); - } - - public IRoom extractRoom(BeforeEnterEvent e, INode node) throws RoomNotFoundException { - var nodeId = node.getIdentifierObject(); - var roomId = RoomIdentifier.of(e.getRouteParameters().get("roomId").orElseThrow(() -> new InstanceNotFoundException("null"))); - return client.getRoomRepository().find(FindRoomById.just(nodeId, roomId)).orElseThrow(() -> new InstanceNotFoundException(roomId.getValue())); - } - - public IUser extractUser(BeforeEnterEvent e) { - var userId = EntityIdentifier.of(e.getRouteParameters().get("userId").orElseThrow(RuntimeException::new)); - return client.getUserRepository().findUserById(FindUserById.withAllDetails(userId)); - } - - public IAccount extractAccount(BeforeEnterEvent e) { - var accountId = e.getRouteParameters().get("accountId").orElseThrow(RuntimeException::new); - return client.getAccountRepository().findAccountByUsername(accountId).orElseThrow(RuntimeException::new); - } -} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/util/UsingSlots.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/UsingSlots.java new file mode 100644 index 0000000..deb3e52 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/util/UsingSlots.java @@ -0,0 +1,12 @@ +package ru.dragonestia.picker.cp.util; + +public class UsingSlots { + + private UsingSlots() {} + + public static int getUsingPercentage(int slots, int usedSlots) { + if (slots == -1) return -1; + double percent = usedSlots / (double) slots * 100; + return (int) percent; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountDetailsPage.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AccountView.java similarity index 60% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountDetailsPage.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/view/AccountView.java index 00810a6..8cceb8f 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/AccountDetailsPage.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AccountView.java @@ -1,4 +1,4 @@ -package ru.dragonestia.picker.cp.page; +package ru.dragonestia.picker.cp.view; import com.vaadin.flow.component.AbstractField; import com.vaadin.flow.component.Html; @@ -8,52 +8,37 @@ import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Hr; -import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.PasswordField; -import com.vaadin.flow.router.BeforeEnterEvent; -import com.vaadin.flow.router.BeforeEnterObserver; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.security.RolesAllowed; -import lombok.RequiredArgsConstructor; -import ru.dragonestia.picker.api.impl.RoomPickerClient; -import ru.dragonestia.picker.api.model.account.IAccount; +import com.vaadin.flow.router.*; +import ru.dragonestia.picker.api.model.account.Account; +import ru.dragonestia.picker.api.model.account.Permission; import ru.dragonestia.picker.cp.component.AccountList; -import ru.dragonestia.picker.cp.component.Notifications; -import ru.dragonestia.picker.cp.model.Permission; -import ru.dragonestia.picker.cp.service.SecurityService; -import ru.dragonestia.picker.cp.util.RouteParamsExtractor; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.Notifications; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; import java.util.ArrayList; -@RequiredArgsConstructor -@RolesAllowed("ADMIN") -@PageTitle("Account details") +@PageTitle("UserDetails details") @Route(value = "/admin/accounts/:accountId", layout = MainLayout.class) -public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObserver { +public class AccountView extends SecuredView{ - private final SecurityService securityService; - private final RouteParamsExtractor paramsExtractor; + private Account account; - private RoomPickerClient client; - private IAccount account; - - @PostConstruct - void postConstruct() { - client = securityService.getAuthenticatedAccount().getClient(); + public AccountView(SessionService sessionService, RouteParamExtractor paramsExtractor) { + super(sessionService, paramsExtractor, Permission.ADMIN); } @Override - public void beforeEnter(BeforeEnterEvent event) { - account = paramsExtractor.extractAccount(event); - - init(); + protected void preRender(RouteParameters routeParams) { + account = getParamsExtractor().account(routeParams); } - private void init() { - add(new H2("Account management")); - add(new Html("Username: %s".formatted(account.getUsername()))); + @Override + protected void render() { + add(new H2("UserDetails management")); + add(new Html("Entityname: %s".formatted(account.id()))); add(new Hr()); add(new H3("Change password")); @@ -91,7 +76,7 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs return; } - client.getAccountRepository().setPassword(account, pass); + getClient().getAccountRepository().changePassword(account, pass); Notifications.success("Password successfully changed!"); newPassword.setValue(""); confirmPassword.setValue(""); @@ -102,9 +87,11 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs private void createEditPermissions() { var permissionsList = new ArrayList(); - for (var permission: Permission.Enum.values()) { + for (var permission: Permission.values()) { + if (permission == Permission.ADMIN) continue; + var comp = new AccountList.PermissionCheckBox(permission); - comp.setValue(account.getPermissions().contains(permission.name())); + comp.setValue(account.permissions().contains(permission)); permissionsList.add(comp); add(comp); } @@ -113,10 +100,9 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs var permissions = permissionsList.stream() .filter(AbstractField::getValue) .map(AccountList.PermissionCheckBox::getOption) - .map(Enum::name) .toList(); - client.getAccountRepository().setPermissions(account, permissions); + getClient().getAccountRepository().setPermissions(account, permissions); Notifications.success("Permissions successfully changed!"); }); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -125,8 +111,8 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs private Button createDeleteAccountButton() { var button = new Button("Delete this account", event -> { - client.getAccountRepository().removeAccount(account); - Notifications.warn("Account '%s' was deleted.".formatted(account.getUsername())); + getClient().getAccountRepository().deleteAccount(account.id()); + Notifications.warn("UserDetails '%s' was deleted.".formatted(account.id())); getUI().ifPresent(ui -> { ui.navigate("/admin/accounts"); diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllAccountsView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllAccountsView.java new file mode 100644 index 0000000..2c589fa --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllAccountsView.java @@ -0,0 +1,25 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import ru.dragonestia.picker.api.model.account.Permission; +import ru.dragonestia.picker.cp.component.AccountList; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +@PageTitle("Accounts") +@Route(value = "/admin/accounts", layout = MainLayout.class) +public class AllAccountsView extends SecuredView { + + public AllAccountsView(SessionService sessionService, RouteParamExtractor paramExtractor) { + super(sessionService, paramExtractor, Permission.ADMIN); + } + + @Override + protected void render() { + add(new H2("Account details management")); + add(new AccountList(getClient())); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllInstancesView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllInstancesView.java new file mode 100644 index 0000000..6a81c86 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/AllInstancesView.java @@ -0,0 +1,55 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.html.Hr; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import ru.dragonestia.picker.api.model.account.Permission; +import ru.dragonestia.picker.api.model.instance.InstanceId; +import ru.dragonestia.picker.cp.component.NavPath; +import ru.dragonestia.picker.cp.component.InstanceList; +import ru.dragonestia.picker.cp.component.RegisterInstance; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +@PageTitle("Instances") +@RouteAlias(value = "/", layout = MainLayout.class) +@Route(value = "/instances", layout = MainLayout.class) +public class AllInstancesView extends SecuredView { + + private InstanceList instanceList; + + public AllInstancesView(SessionService sessionService, RouteParamExtractor paramExtractor) { + super(sessionService, paramExtractor); + } + + protected RegisterInstance createRegisterInstanceElement() { + return new RegisterInstance(instance -> { + try { + getClient().getInstanceRepository().createInstance(InstanceId.of(instance.getId()), instance.getMethod(), instance.isPersist()); + return new RegisterInstance.Response(false, ""); + } catch (Exception ex) { + return new RegisterInstance.Response(true, ex.getMessage()); + } finally { + instanceList.refresh(); + } + }); + } + + protected InstanceList createInstanceListElement() { + return new InstanceList(getClient()); + } + + @Override + protected void render() { + add(NavPath.rootInstances()); + + if (getSessionService().hasPermission(Permission.NODE_MANAGEMENT)) { + add(createRegisterInstanceElement()); + } + + add(new Hr()); + add(instanceList = createInstanceListElement()); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/EntityView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/EntityView.java new file mode 100644 index 0000000..73c92e1 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/EntityView.java @@ -0,0 +1,80 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.H3; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteParameters; +import ru.dragonestia.picker.cp.component.RefreshableTable; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; +import ru.dragonestia.picker.cp.repository.dto.RoomDTO; +import ru.dragonestia.picker.cp.repository.graphql.EntityRooms; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +import java.util.List; + +@PageTitle("Entity details") +@Route(value = "/entities/:entityId", layout = MainLayout.class) +public class EntityView extends SecuredView implements RefreshableTable { + + private EntityDTO entity; + private Grid gridRooms; + + public EntityView(SessionService service, RouteParamExtractor paramExtractor) { + super(service, paramExtractor); + } + + @Override + protected void preRender(RouteParameters routeParams) { + entity = getParamsExtractor().entity(routeParams); + } + + @Override + protected void render() { + add(new H2("Entity '%s'".formatted(entity.getId()))); + add(new H3("Linked with rooms")); + add(gridRooms = createGrid()); + + refresh(); + } + + private Grid createGrid() { + var grid = new Grid(); + + grid.addColumn(RoomDTO::getId).setHeader("Room identifier").setSortable(true); + + grid.addColumn(RoomDTO::getInstanceId).setHeader("Instance identifier").setSortable(true); + + grid.addColumn(RoomDTO::getCountEntities).setHeader("Entities") + .setComparator((room1, room2) -> { + var r1 = room1.getCountEntities(); + var r2 = room2.getCountEntities(); + + return Integer.compare(r1, r2); + }).setSortable(true); + + grid.addComponentColumn(room -> { + var button = new Button("Details"); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.addClickListener(event -> { + getUI().ifPresent(ui -> ui.navigate("/instances/%s/rooms/%s".formatted(room.getInstanceId(), room.getId()))); + }); + return button; + }).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true).setHeader(createRefreshButton()); + + grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); + return grid; + } + + @Override + public void refresh() { + List cachedRooms = getClient().getRestTemplate().executeGraphQL(EntityRooms.query(entity.getId())).getEntityById().getRooms().stream().map(entity -> (RoomDTO) entity).toList(); + gridRooms.setItems(cachedRooms); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/InstanceView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/InstanceView.java new file mode 100644 index 0000000..46cdccd --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/InstanceView.java @@ -0,0 +1,60 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Hr; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.*; +import ru.dragonestia.picker.api.model.instance.Instance; +import ru.dragonestia.picker.cp.component.RoomList; +import ru.dragonestia.picker.cp.component.NavPath; +import ru.dragonestia.picker.cp.component.RegisterRoom; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +@PageTitle("Rooms") +@Route(value = "/instances/:instanceId", layout = MainLayout.class) +public class InstanceView extends SecuredView { + + private Instance instance; + private RoomList roomList; + + public InstanceView(SessionService sessionService, RouteParamExtractor paramsExtractor) { + super(sessionService, paramsExtractor); + } + + @Override + protected void preRender(RouteParameters routeParams) { + instance = getParamsExtractor().instance(routeParams); + } + + @Override + protected void render() { + add(NavPath.toInstance(instance.id().getValue())); + printInstanceDetails(instance); + add(new Hr()); + add(new RegisterRoom(instance, room -> { + try { + getClient().getRoomRepository().createRoom(room.instanceId(), room.id(), room.slots(), room.payload(), room.locked(), room.persist()); + return new RegisterRoom.Response(false, null); + } catch (Error error) { + return new RegisterRoom.Response(true, error.getMessage()); + } finally { + roomList.refresh(); + } + })); + add(new Hr()); + add(roomList = new RoomList(instance, getClient())); + } + + private void printInstanceDetails(Instance instance) { + add(new H2("Instance details")); + + var layout = new VerticalLayout(); + layout.add(new Html("Identifier: " + instance.id() + "")); + layout.add(new Html("Mode: " + instance.method().name() + "")); + + add(layout); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/LoginView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/LoginView.java new file mode 100644 index 0000000..1654d68 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/LoginView.java @@ -0,0 +1,81 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.Unit; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.impl.exception.AuthException; +import ru.dragonestia.picker.cp.annotation.ServerURL; +import ru.dragonestia.picker.cp.model.AccountSession; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.Notifications; + +@Route("/login") +@PageTitle("Log in") +public class LoginView extends VerticalLayout { + + private final SessionService sessionService; + private final String serverUrl; + private final TextField fieldLogin; + private final PasswordField fieldPassword; + + public LoginView(SessionService sessionService, @ServerURL String serverURL) { + this.sessionService = sessionService; + this.serverUrl = serverURL; + + if (sessionService.getSession() != null) { + fieldLogin = null; + fieldPassword = null; + + add(new Button("Logout", event -> getUI().ifPresent(sessionService::logout))); + return; + } + + setAlignItems(Alignment.CENTER); + + add(new Html("

RoomPicker!

")); + add(fieldLogin = createLoginField()); + add(fieldPassword = createPasswordField()); + add(createLoginButton()); + } + + private TextField createLoginField() { + var field = new TextField("Username"); + field.setRequired(true); + field.setMinWidth(20, Unit.PERCENTAGE); + return field; + } + + private PasswordField createPasswordField() { + var field = new PasswordField("Password"); + field.setRequired(true); + field.setMinWidth(20, Unit.PERCENTAGE); + return field; + } + + private Button createLoginButton() { + var button = new Button("Login", event -> tryLogin()); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.setMinWidth(20, Unit.PERCENTAGE); + return button; + } + + private void tryLogin() { + var client = new RoomPickerClient(serverUrl, fieldLogin.getValue(), fieldPassword.getValue()); + try { + var account = client.getAccount(); + var session = new AccountSession(account, fieldPassword.getValue(), client); + getUI().ifPresent(ui -> { + sessionService.login(session, ui); + }); + } catch (AssertionError | AuthException ex) { + Notifications.error("Invalid username or password"); + } + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/RoomView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/RoomView.java new file mode 100644 index 0000000..08c8933 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/RoomView.java @@ -0,0 +1,127 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.Html; +import com.vaadin.flow.component.Unit; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Hr; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextArea; +import com.vaadin.flow.router.*; +import ru.dragonestia.picker.api.model.entity.EntityId; +import ru.dragonestia.picker.api.model.room.Room; +import ru.dragonestia.picker.cp.component.AddEntities; +import ru.dragonestia.picker.cp.component.NavPath; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.Notifications; +import ru.dragonestia.picker.cp.component.EntityList; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +@PageTitle("Room details") +@Route(value = "/instances/:instanceId/rooms/:roomId", layout = MainLayout.class) +public class RoomView extends SecuredView { + + private Room room; + private EntityList entityList; + private Button lockRoomButton; + private VerticalLayout roomInfo; + + public RoomView(SessionService sessionService, RouteParamExtractor paramExtractor) { + super(sessionService, paramExtractor); + } + + @Override + protected void preRender(RouteParameters routeParams) { + room = getParamsExtractor().room(routeParams); + } + + @Override + protected void render() { + add(NavPath.toRoom(room.instanceId().getValue(), room.id().getValue())); + add(new H2("Room details")); + printRoomDetails(); + add(new Hr()); + add(new AddEntities(room, (entities, ignoreLimitation) -> appendEntities(room, entities, ignoreLimitation))); + add(new Hr()); + add(new H2("Entities")); + add(entityList = new EntityList(room, getClient())); + } + + private void updateRoomInfo() { + roomInfo.removeAll(); + roomInfo.add(new Html("Instance identifier: " + room.instanceId() + "")); + roomInfo.add(new Html("Room identifier: " + room.id() + "")); + roomInfo.add(new Html("Slots: " + (room.slots() == -1? "Unlimited" : room.slots()) + "")); + roomInfo.add(new Html("Locked: " + (room.locked()? "Yes" : "No") + "")); + } + + private void printRoomDetails() { + add(roomInfo = new VerticalLayout()); + roomInfo.setPadding(false); + + updateRoomInfo(); + add(lockRoomButton = new Button("", event -> changeBucketLockedState())); + setLockRoomButtonState(); + + var payload = new TextArea("Payload(" + room.payload().length() + ")"); + payload.setValue(room.payload()); + payload.setReadOnly(true); + payload.setMinWidth(50, Unit.REM); + add(payload); + } + + private void setLockRoomButtonState() { + if (room.locked()) { + lockRoomButton.setText("Unlock"); + lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.UNLOCK)); + } else { + lockRoomButton.setText("Lock"); + lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.LOCK)); + } + } + + private void changeBucketLockedState() { + var newValue = !room.locked(); + getClient().getRoomRepository().lockRoom(room, newValue); + + room = getClient().getRoomRepository().getRoom(room.instanceId(), room.id()); + + setLockRoomButtonState(); + updateRoomInfo(); + + Notifications.success("Success"); + } + + private void appendEntities(Room room, Collection entities, boolean ignoreLimitation) { + AtomicBoolean validationFail = new AtomicBoolean(false); + + var newEntities = entities.stream() + .filter(entity -> { + if (entity.getValue().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) { + return true; + } + + validationFail.set(true); + return false; + }).toList(); + + getClient().getEntityRepository().linkEntitiesWithRoom(room, entities, ignoreLimitation); + entityList.refresh(); + + if (validationFail.get()) { + if (newEntities.isEmpty()) { + Notifications.error("All entities entered were added because they do not comply with the rule for writing the entity identifier"); + } else { + Notifications.warn("Not all entities entered were added because they do not comply with the rule for writing the entity identifier"); + } + } else { + Notifications.success("Success"); + } + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SearchEntityView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SearchEntityView.java new file mode 100644 index 0000000..1c04860 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SearchEntityView.java @@ -0,0 +1,110 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.Unit; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.ColumnTextAlign; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import org.springframework.beans.factory.annotation.Autowired; +import ru.dragonestia.picker.cp.component.RefreshableTable; +import ru.dragonestia.picker.cp.repository.dto.EntityDTO; +import ru.dragonestia.picker.cp.repository.graphql.SearchEntity; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; +import ru.dragonestia.picker.cp.view.layout.MainLayout; + +import java.util.LinkedList; +import java.util.List; + +@PageTitle("Search entities") +@Route(value = "/entities", layout = MainLayout.class) +public class SearchEntityView extends SecuredView implements RefreshableTable { + + private TextField fieldEntityname; + private Grid entityGrid; + private Span foundEntities; + private List cachedEntities = new LinkedList<>(); + + @Autowired + public SearchEntityView(SessionService sessionService, RouteParamExtractor paramExtractor) { + super(sessionService, paramExtractor); + } + + private TextField createEntitynameInputField() { + var field = new TextField(); + field.setLabel("Entityname"); + field.setPlaceholder("some-entity-identifier"); + field.setRequired(true); + field.setMinWidth(30, Unit.PERCENTAGE); + field.setAutofocus(true); + + var button = new Button(new Icon(VaadinIcon.SEARCH)); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.getStyle().set("color", "#FFFFFF"); + button.addClickListener(event -> refresh()); + button.addClickShortcut(Key.ENTER); + + field.setSuffixComponent(button); + return field; + } + + private Grid createEntityGrid() { + var grid = new Grid(); + + grid.addColumn(EntityDTO::getId).setHeader("Identifier").setSortable(true) + .setFooter(foundEntities); + + grid.addColumn(EntityDTO::getCountRooms).setComparator((entity1, entity2) -> { + var r1 = entity1.getCountRooms(); + var r2 = entity2.getCountRooms(); + + return Integer.compare(r1, r2); + }).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with rooms"); + + grid.addComponentColumn(entity -> { + var button = new Button("Details"); + button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + button.addClickListener(event -> { + getUI().ifPresent(ui -> ui.navigate("/entities/" + entity.getId())); + }); + return button; + }).setTextAlign(ColumnTextAlign.END).setFrozenToEnd(true).setHeader(createRefreshButton()); + + grid.setMultiSort(true, Grid.MultiSortPriority.APPEND); + return grid; + } + + private void search(String input) { + if (input.isEmpty()) { + entityGrid.setItems(); + } + + entityGrid.setItems(cachedEntities = getClient().getRestTemplate().executeGraphQL(SearchEntity.query(input)).getSearchEntity().stream().map(entity -> (EntityDTO) entity).toList()); + } + + @Override + public void refresh() { + search(fieldEntityname.getValue().trim()); + foundEntities.setText("Found %s entities".formatted(cachedEntities.size())); + } + + public void justRefresh() { + entityGrid.setItems(); + foundEntities.setText("Found %s entities".formatted(cachedEntities.size())); + } + + @Override + protected void render() { + foundEntities = new Span(); + add(fieldEntityname = createEntitynameInputField()); + add(entityGrid = createEntityGrid()); + justRefresh(); + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SecuredView.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SecuredView.java new file mode 100644 index 0000000..b1b5570 --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/SecuredView.java @@ -0,0 +1,84 @@ +package ru.dragonestia.picker.cp.view; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.RouteParameters; +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.account.Permission; +import ru.dragonestia.picker.cp.exception.Unauthorized; +import ru.dragonestia.picker.cp.model.AccountSession; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.util.RouteParamExtractor; + +public abstract class SecuredView extends VerticalLayout implements BeforeEnterObserver { + + private final SessionService sessionService; + private final RouteParamExtractor paramsExtractor; + private final AccountSession accountSession; + private final RoomPickerClient client; + private final boolean authenticated; + + public SecuredView(SessionService sessionService, RouteParamExtractor paramsExtractor, Permission... requiredPermissions) { + this.sessionService = sessionService; + this.paramsExtractor = paramsExtractor; + this.accountSession = sessionService.getSession(); + + boolean auth = true; + try { + checkAuth(); + checkPermissions(requiredPermissions); + } catch (Unauthorized ex) { + auth = false; + } + authenticated = auth; + + client = auth? accountSession.getClient() : null; + } + + public final SessionService getSessionService() { + return sessionService; + } + + public final AccountSession getAccountSession() { + return accountSession; + } + + public final RoomPickerClient getClient() { + return client; + } + + public RouteParamExtractor getParamsExtractor() { + return paramsExtractor; + } + + private void checkAuth() throws Unauthorized { + if (accountSession == null || accountSession.getData() == null) { + throw new Unauthorized(); + } + } + + private void checkPermissions(Permission... permissions) throws Unauthorized { + for (var permission: permissions) { + if (!accountSession.getData().permissions().contains(permission)) { + throw new Unauthorized(); + } + } + } + + @Override + public final void beforeEnter(BeforeEnterEvent event) { + if (!authenticated) { + UI.getCurrent().getPage().setLocation("/login"); + return; + } + + preRender(event.getRouteParameters()); + render(); + } + + protected void preRender(RouteParameters routeParams) {} + + protected abstract void render(); +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/MainLayout.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/layout/MainLayout.java similarity index 51% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/page/MainLayout.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/view/layout/MainLayout.java index 9435d1b..239698d 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/MainLayout.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/layout/MainLayout.java @@ -1,4 +1,4 @@ -package ru.dragonestia.picker.cp.page; +package ru.dragonestia.picker.cp.view.layout; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Html; @@ -16,36 +16,45 @@ import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNavItem; -import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.account.Permission; import ru.dragonestia.picker.api.repository.response.RoomPickerInfoResponse; import ru.dragonestia.picker.cp.annotation.ServerURL; -import ru.dragonestia.picker.cp.model.Account; -import ru.dragonestia.picker.cp.service.SecurityService; +import ru.dragonestia.picker.cp.model.AccountSession; +import ru.dragonestia.picker.cp.service.SessionService; +import ru.dragonestia.picker.cp.view.AllAccountsView; +import ru.dragonestia.picker.cp.view.AllInstancesView; +import ru.dragonestia.picker.cp.view.SearchEntityView; public class MainLayout extends AppLayout { - private final SecurityService securityService; - private final RoomPickerInfoResponse serverInfo; + private final SessionService sessionService; private final String serverUrl; - private final Account account; + private final AccountSession session; + private final RoomPickerInfoResponse info; private final boolean isAdmin; - public MainLayout(SecurityService securityService, RoomPickerClient adminClient, @ServerURL String serverUrl) { - this.securityService = securityService; - this.serverInfo = adminClient.getServerInfo(); - this.serverUrl = serverUrl; - account = securityService.getAuthenticatedAccount(); - isAdmin = securityService.hasRole("ADMIN"); + public MainLayout(SessionService sessionService, @ServerURL String serverURL) { + this.sessionService = sessionService; + this.serverUrl = serverURL; + this.session = sessionService.getSession(); - var toggle = new DrawerToggle(); - var scroller = new Scroller(createSideNav()); - scroller.setWidth(100, Unit.PERCENTAGE); + if (session == null) { + info = null; + isAdmin = false; + } else { + info = session.getClient().getServerInfo(); + isAdmin = session.getData().permissions().contains(Permission.ADMIN); - var navLayout = new VerticalLayout(createAccountButtons(), new Hr(), scroller); - navLayout.setPadding(false); + var toggle = new DrawerToggle(); + var scroller = new Scroller(createSideNav()); + scroller.setWidth(100, Unit.PERCENTAGE); - addToDrawer(navLayout); - addToNavbar(toggle, createLogo()); + var navLayout = new VerticalLayout(createAccountButtons(), new Hr(), scroller); + navLayout.setPadding(false); + + addToDrawer(navLayout); + addToNavbar(toggle, createLogo()); + } } private Component createLogo() { @@ -53,17 +62,17 @@ public class MainLayout extends AppLayout { layout.setAlignItems(FlexComponent.Alignment.END); layout.setPadding(true); layout.add(new Html("

RoomPicker!

")); - layout.add(new Html("" + serverInfo.version() + "")); + layout.add(new Html("" + info.version() + "")); return layout; } private Component createAccountButtons() { var layout = new VerticalLayout(); var username = new Span(new Icon(isAdmin? VaadinIcon.USER_STAR : VaadinIcon.USER)); - username.add(account.getUsername()); + username.add(session.getData().id().getValue()); layout.add(username); - var logoutButton = new Button("Logout", event -> securityService.logout()); + var logoutButton = new Button("Logout", event -> getUI().ifPresent(sessionService::logout)); logoutButton.setWidth(100, Unit.PERCENTAGE); layout.add(logoutButton); @@ -72,10 +81,10 @@ public class MainLayout extends AppLayout { private SideNav createSideNav() { var nav = new SideNav(); - nav.addItem(new SideNavItem("Nodes list", NodesPage.class, VaadinIcon.FOLDER_O.create())); - nav.addItem(new SideNavItem("Search users", UserSearchPage.class, VaadinIcon.SEARCH.create())); + nav.addItem(new SideNavItem("Instances list", AllInstancesView.class, VaadinIcon.FOLDER_O.create())); + nav.addItem(new SideNavItem("Search entities", SearchEntityView.class, VaadinIcon.SEARCH.create())); if (isAdmin) { - nav.addItem(new SideNavItem("Accounts", AccountsPage.class, VaadinIcon.USERS.create())); + nav.addItem(new SideNavItem("Accounts", AllAccountsView.class, VaadinIcon.USERS.create())); } nav.addItem(new SideNavItem("Documentation", "https://github.com/ScarletRedMan/RoomPicker", VaadinIcon.BOOK.create())); nav.addItem(new SideNavItem("Swagger UI", serverUrl + "/api-docs-ui", VaadinIcon.CURLY_BRACKETS.create())); diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/ErrorPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/ErrorPlug.java similarity index 69% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/ErrorPlug.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/ErrorPlug.java index 955f937..139deb2 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/ErrorPlug.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/ErrorPlug.java @@ -1,17 +1,16 @@ -package ru.dragonestia.picker.cp.page.plug; +package ru.dragonestia.picker.cp.view.plug; import com.vaadin.flow.component.Html; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.ParentLayout; import ru.dragonestia.picker.cp.component.NavPath; -import ru.dragonestia.picker.cp.page.MainLayout; +import ru.dragonestia.picker.cp.view.layout.MainLayout; @ParentLayout(MainLayout.class) public abstract class ErrorPlug extends VerticalLayout { - public void init(NavPath path, String title, String description) { - add(path); + public void init(String title, String description) { add(new H1(title)); add(new Html("

" + description + "

")); } diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/InvalidIdentifierPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/InvalidIdentifierPlug.java new file mode 100644 index 0000000..d147bfe --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/InvalidIdentifierPlug.java @@ -0,0 +1,17 @@ +package ru.dragonestia.picker.cp.view.plug; + +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import jakarta.servlet.http.HttpServletResponse; +import ru.dragonestia.picker.api.exception.InvalidIdentifierException; + +public class InvalidIdentifierPlug extends ErrorPlug implements HasErrorParameter { + + @Override + public int setErrorParameter(BeforeEnterEvent event, ErrorParameter parameter) { + var ex = parameter.getException(); + init("Error 400", ex.getMessage()); + return HttpServletResponse.SC_NOT_FOUND; + } +} diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NotEnoughPermissionsPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotEnoughPermissionsPlug.java similarity index 89% rename from control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NotEnoughPermissionsPlug.java rename to control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotEnoughPermissionsPlug.java index 55c860e..42d6c2c 100644 --- a/control-panel/src/main/java/ru/dragonestia/picker/cp/page/plug/NotEnoughPermissionsPlug.java +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotEnoughPermissionsPlug.java @@ -1,4 +1,4 @@ -package ru.dragonestia.picker.cp.page.plug; +package ru.dragonestia.picker.cp.view.plug; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Paragraph; @@ -6,7 +6,6 @@ import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.ErrorParameter; import com.vaadin.flow.router.HasErrorParameter; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.userdetails.User; import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions; public class NotEnoughPermissionsPlug extends ErrorPlug implements HasErrorParameter { diff --git a/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotFoundPlug.java b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotFoundPlug.java new file mode 100644 index 0000000..e454cce --- /dev/null +++ b/control-panel/src/main/java/ru/dragonestia/picker/cp/view/plug/NotFoundPlug.java @@ -0,0 +1,17 @@ +package ru.dragonestia.picker.cp.view.plug; + +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; +import jakarta.servlet.http.HttpServletResponse; +import ru.dragonestia.picker.api.exception.DoesNotExistsException; + +public class NotFoundPlug extends ErrorPlug implements HasErrorParameter { + + @Override + public int setErrorParameter(BeforeEnterEvent event, ErrorParameter parameter) { + var ex = parameter.getException(); + init("Error 404", ex.getMessage()); + return HttpServletResponse.SC_NOT_FOUND; + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java b/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java index 71bf29c..a5efd38 100644 --- a/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java +++ b/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java @@ -59,20 +59,20 @@ public class UserMetricsAspect { void onCreateNode(Instance instance) { var nodeId = instance.getId(); var gauge = Gauge.builder("roompicker_node_users_total", () -> data.get(nodeId.getValue()).users()) - .tag("nodeId", nodeId.getValue()) + .tag("instanceId", nodeId.getValue()) .register(meterRegistry); var counter = Counter.builder("roompicker_picks") - .tag("nodeId", nodeId.getValue()) + .tag("instanceId", nodeId.getValue()) .baseUnit("1s") .register(meterRegistry); var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId.getValue()).locked()) - .tag("nodeId", nodeId.getValue()) + .tag("instanceId", nodeId.getValue()) .register(meterRegistry); var roomsGauge = Gauge.builder("roompicker_rooms", () -> roomRepository.all(instance.getId()).size()) - .tag("nodeId", nodeId.getValue()) + .tag("instanceId", nodeId.getValue()) .register(meterRegistry); data.put(nodeId.getValue(), new NodeData(gauge, new AtomicInteger(0), counter, new AtomicInteger(0), lockedGauge, roomsGauge));