refactored control panel

This commit is contained in:
Andrey Terentev 2024-05-27 09:49:47 +07:00 committed by Andrey Terentev
parent 0c19050436
commit 93226022b2
67 changed files with 1620 additions and 1539 deletions

View File

@ -30,7 +30,6 @@ dependencies {
implementation project(":client-impl") implementation project(":client-impl")
implementation 'com.vaadin:vaadin-spring-boot-starter' implementation 'com.vaadin:vaadin-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'

View File

@ -17,23 +17,25 @@ import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.theme.lumo.LumoIcon; import com.vaadin.flow.theme.lumo.LumoIcon;
import ru.dragonestia.picker.api.model.account.ResponseAccount; import ru.dragonestia.picker.api.impl.RoomPickerClient;
import ru.dragonestia.picker.api.repository.AccountRepository; import ru.dragonestia.picker.api.model.account.Account;
import ru.dragonestia.picker.cp.model.Permission; 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.*;
import java.util.stream.Collectors;
public class AccountList extends VerticalLayout implements RefreshableTable { public class AccountList extends VerticalLayout implements RefreshableTable {
private final AccountRepository accountRepository; private final RoomPickerClient client;
private final TextField searchField; private final TextField searchField;
private final Grid<ResponseAccount> grid; private final Grid<Account> grid;
private List<ResponseAccount> cachedAccounts = new ArrayList<>(); private List<Account> cachedAccounts = new ArrayList<>();
public AccountList(AccountRepository accountRepository) { public AccountList(RoomPickerClient client) {
this.accountRepository = accountRepository; this.client = client;
add(searchField = createSearchField()); add(searchField = createSearchField());
add(grid = createGridAccounts()); add(grid = createGridAccounts());
@ -42,7 +44,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
} }
private TextField createSearchField() { 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.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
field.setClearButtonVisible(true); field.setClearButtonVisible(true);
field.setHelperText("Press Enter to search"); field.setHelperText("Press Enter to search");
@ -52,11 +54,11 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
return field; return field;
} }
private Grid<ResponseAccount> createGridAccounts() { private Grid<Account> createGridAccounts() {
var grid = new Grid<>(ResponseAccount.class, false); var grid = new Grid<>(Account.class, false);
grid.addColumn(ResponseAccount::getUsername).setHeader("Username") grid.addColumn(Account::id).setHeader("Username")
.setComparator(Comparator.comparing(ResponseAccount::getUsername)).setSortable(true); .setComparator(Comparator.comparing(account -> account.id().getValue())).setSortable(true);
grid.addComponentColumn(this::createAccountManagementButtons).setFrozenToEnd(true) grid.addComponentColumn(this::createAccountManagementButtons).setFrozenToEnd(true)
.setTextAlign(ColumnTextAlign.END).setHeader(createToolItems()); .setTextAlign(ColumnTextAlign.END).setHeader(createToolItems());
@ -66,7 +68,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
return grid; return grid;
} }
private HorizontalLayout createAccountManagementButtons(ResponseAccount account) { private HorizontalLayout createAccountManagementButtons(Account account) {
var layout = new HorizontalLayout(JustifyContentMode.END); var layout = new HorizontalLayout(JustifyContentMode.END);
{ {
@ -74,7 +76,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
button.addClickListener(event -> { button.addClickListener(event -> {
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.navigate("/admin/accounts/" + account.getUsername()); ui.navigate("/admin/accounts/" + account.id());
}); });
}); });
layout.add(button); layout.add(button);
@ -101,7 +103,8 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
@Override @Override
public void refresh() { public void refresh() {
cachedAccounts = accountRepository.allAccounts(); var ids = client.getAccountRepository().allAccountsIds();
cachedAccounts = client.getAccountRepository().getAccounts(ids);
applySearch(searchField.getValue()); applySearch(searchField.getValue());
} }
@ -109,7 +112,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
var temp = input.trim(); var temp = input.trim();
grid.setItems(cachedAccounts.stream() grid.setItems(cachedAccounts.stream()
.filter(account -> account.getUsername().startsWith(temp)) .filter(account -> account.id().getValue().startsWith(temp))
.toList()); .toList());
} }
@ -123,9 +126,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
var layout = new VerticalLayout(); var layout = new VerticalLayout();
var fieldUsername = new TextField("Account username"); var fieldEntityname = new TextField("Username");
fieldUsername.setWidth(70, Unit.PERCENTAGE); fieldEntityname.setWidth(70, Unit.PERCENTAGE);
layout.add(fieldUsername); layout.add(fieldEntityname);
var fieldPassword = new PasswordField("Password"); var fieldPassword = new PasswordField("Password");
fieldPassword.setWidth(70, Unit.PERCENTAGE); fieldPassword.setWidth(70, Unit.PERCENTAGE);
@ -138,7 +141,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
layout.add(new H3("Permissions")); layout.add(new H3("Permissions"));
var permissionsList = new ArrayList<PermissionCheckBox>(); var permissionsList = new ArrayList<PermissionCheckBox>();
for (var permission: Permission.Enum.values()) { for (var permission: Permission.values()) {
if (permission == Permission.ADMIN) continue;
var comp = new PermissionCheckBox(permission); var comp = new PermissionCheckBox(permission);
permissionsList.add(comp); permissionsList.add(comp);
layout.add(comp); layout.add(comp);
@ -149,7 +154,7 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
button.setWidth(100, Unit.PERCENTAGE); button.setWidth(100, Unit.PERCENTAGE);
button.addClickListener(event -> { button.addClickListener(event -> {
validateAndRegister(dialog, fieldUsername, fieldPassword, fieldConfirmPassword, permissionsList); validateAndRegister(dialog, fieldEntityname, fieldPassword, fieldConfirmPassword, permissionsList);
}); });
dialog.getFooter().add(button); dialog.getFooter().add(button);
} }
@ -162,13 +167,13 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
dialog.open(); dialog.open();
} }
private void validateAndRegister(Dialog dialog, TextField usernameField, PasswordField passwordField, PasswordField confirmPasswordField, List<PermissionCheckBox> permissionCheckBoxes) { private void validateAndRegister(Dialog dialog, TextField entitynameField, PasswordField passwordField, PasswordField confirmPasswordField, List<PermissionCheckBox> permissionCheckBoxes) {
var username = usernameField.getValue().trim(); var entityname = entitynameField.getValue().trim();
var password = passwordField.getValue(); var password = passwordField.getValue();
var confirmPassword = confirmPasswordField.getValue(); var confirmPassword = confirmPasswordField.getValue();
if (username.length() < 3 || username.length() > 32) { if (entityname.length() < 3 || entityname.length() > 32) {
Notifications.error("Invalid username length. Valid is 3-32"); Notifications.error("Invalid entityname length. Valid is 3-32");
return; return;
} }
@ -185,10 +190,9 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
var permissions = permissionCheckBoxes.stream() var permissions = permissionCheckBoxes.stream()
.filter(AbstractField::getValue) .filter(AbstractField::getValue)
.map(PermissionCheckBox::getOption) .map(PermissionCheckBox::getOption)
.map(Enum::name) .toList();
.collect(Collectors.toSet());
accountRepository.createAccount(username, password, permissions); client.getAccountRepository().createAccount(AccountId.of(entityname), password, permissions);
dialog.close(); dialog.close();
refresh(); refresh();
@ -196,14 +200,14 @@ public class AccountList extends VerticalLayout implements RefreshableTable {
public static class PermissionCheckBox extends Checkbox { public static class PermissionCheckBox extends Checkbox {
private final Permission.Enum option; private final Permission option;
public PermissionCheckBox(Permission.Enum option) { public PermissionCheckBox(Permission option) {
super(option.getDescription()); super(PermissionDescription.of(option));
this.option = option; this.option = option;
} }
public Permission.Enum getOption() { public Permission getOption() {
return option; return option;
} }
} }

View File

@ -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<Collection<EntityId>, Boolean> onCommit;
private final Checkbox ignoreSlots;
private final VerticalLayout entitiesLayout;
private final AtomicInteger freeEntityIdNumber = new AtomicInteger(1);
public AddEntities(Room room, BiConsumer<Collection<EntityId>, 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<EntityId> 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;
}
}
}

View File

@ -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<Collection<IUser>, Boolean> onCommit;
private final Checkbox ignoreSlots;
private final VerticalLayout usersLayout;
private final AtomicInteger freeUserIdNumber = new AtomicInteger(1);
public AddUsers(IRoom room, BiConsumer<Collection<IUser>, 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<IUser> 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;
}
}
}

View File

@ -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<EntityDTO> entitiesGrid;
private final Span totalEntities = new Span();
private final Span occupancy = new Span();
private List<EntityDTO> 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<EntityDTO> createEntitiesGrid() {
var grid = new Grid<EntityDTO>();
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()) + "%"));
}
}

View File

@ -14,34 +14,34 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.data.value.ValueChangeMode;
import ru.dragonestia.picker.api.model.node.INode; import ru.dragonestia.picker.api.impl.RoomPickerClient;
import ru.dragonestia.picker.api.model.node.NodeDetails; import ru.dragonestia.picker.api.model.instance.InstanceId;
import ru.dragonestia.picker.api.repository.InstanceRepository; import ru.dragonestia.picker.cp.repository.dto.InstanceDTO;
import ru.dragonestia.picker.api.repository.query.node.GetAllNodes; import ru.dragonestia.picker.cp.repository.graphql.AllInstances;
import ru.dragonestia.picker.cp.util.Notifications;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
public class NodeList extends VerticalLayout implements RefreshableTable { public class InstanceList extends VerticalLayout implements RefreshableTable {
private final InstanceRepository instanceRepository; private final RoomPickerClient client;
private final Grid<INode> nodesGrid; private final Grid<InstanceDTO> instancesGrid;
private final TextField searchField; private final TextField searchField;
private List<INode> cachedNodes; private List<InstanceDTO> cachedInstances;
public NodeList(InstanceRepository instanceRepository) { public InstanceList(RoomPickerClient client) {
super(); this.client = client;
this.instanceRepository = instanceRepository;
add(new H2("Nodes")); add(new H2("Instances"));
add(searchField = createSearchField()); add(searchField = createSearchField());
add(nodesGrid = createGrid()); add(instancesGrid = createGrid());
refresh(); refresh();
} }
private TextField createSearchField() { private TextField createSearchField() {
var field = new TextField("Search node"); var field = new TextField("Search instance");
field.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); field.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
field.setClearButtonVisible(true); field.setClearButtonVisible(true);
field.setHelperText("Press Enter to search"); field.setHelperText("Press Enter to search");
@ -53,25 +53,28 @@ public class NodeList extends VerticalLayout implements RefreshableTable {
private void applySearch(String input) { private void applySearch(String input) {
var temp = input.trim(); var temp = input.trim();
nodesGrid.setItems(cachedNodes.stream() var instances = cachedInstances.stream()
.filter(node -> node.getIdentifier().startsWith(temp)) .filter(instance -> instance.getId().startsWith(temp))
.toList()); .map(instance -> (InstanceDTO) instance)
.toList();
instancesGrid.setItems(instances);
} }
private Grid<INode> createGrid() { private Grid<InstanceDTO> createGrid() {
var grid = new Grid<>(INode.class, false); var grid = new Grid<>(InstanceDTO.class, false);
grid.addComponentColumn(node -> { grid.addComponentColumn(instance -> {
if (Boolean.parseBoolean(node.getDetail(NodeDetails.PERSIST))) { if (instance.isPersist()) {
return new Span(node.getIdentifier()); return new Span(instance.getId());
} }
var result = new Span(node.getIdentifier()); var result = new Span(instance.getId());
result.add(grayBadge("(temp)")); result.add(grayBadge("(temp)"));
return result; 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) grid.addComponentColumn(this::createManageButtons).setFrozenToEnd(true)
.setTextAlign(ColumnTextAlign.END).setHeader(createRefreshButton()); .setTextAlign(ColumnTextAlign.END).setHeader(createRefreshButton());
@ -80,33 +83,33 @@ public class NodeList extends VerticalLayout implements RefreshableTable {
return grid; return grid;
} }
private HorizontalLayout createManageButtons(INode node) { private HorizontalLayout createManageButtons(InstanceDTO instance) {
var layout = new HorizontalLayout(JustifyContentMode.END); var layout = new HorizontalLayout(JustifyContentMode.END);
{ {
var button = new Button("Details"); var button = new Button("Details");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
button.addClickListener(event -> clickDetailsButton(node)); button.addClickListener(event -> clickDetailsButton(instance));
layout.add(button); layout.add(button);
} }
{ {
var button = new Button("Remove"); var button = new Button("Remove");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> clickRemoveButton(node)); button.addClickListener(event -> clickRemoveButton(instance));
layout.add(button); layout.add(button);
} }
return layout; return layout;
} }
private void clickDetailsButton(INode node) { private void clickDetailsButton(InstanceDTO instance) {
getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.getIdentifier())); getUI().ifPresent(ui -> ui.navigate("/instances/" + instance.getId()));
} }
private void clickRemoveButton(INode node) { private void clickRemoveButton(InstanceDTO instance) {
var dialog = new Dialog("Confirm node deletion"); var dialog = new Dialog("Confirm instance deletion");
dialog.add(new Html("<p>Confirm that you want to delete node. Enter <b><u>" + node.getIdentifier() + "</u></b> to field below and confirm.</p>")); dialog.add(new Html("<p>Confirm that you want to delete instance. Enter <b><u>" + instance.getId() + "</u></b> to field below and confirm.</p>"));
var inputField = new TextField(); var inputField = new TextField();
inputField.setWidth("100%"); inputField.setWidth("100%");
@ -116,13 +119,13 @@ public class NodeList extends VerticalLayout implements RefreshableTable {
var button = new Button("Confirm"); var button = new Button("Confirm");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> { button.addClickListener(event -> {
if (!node.getIdentifier().equals(inputField.getValue())) { if (!instance.getId().equals(inputField.getValue())) {
Notifications.error("Invalid input"); Notifications.error("Invalid input");
return; return;
} }
removeNode(node); removeInstance(instance);
Notifications.success("Node <b>" + node.getIdentifier() + "</b> was successfully removed!"); Notifications.success("Instance <b>" + instance.getId() + "</b> was successfully removed!");
dialog.close(); dialog.close();
}); });
@ -138,14 +141,16 @@ public class NodeList extends VerticalLayout implements RefreshableTable {
dialog.open(); dialog.open();
} }
private void removeNode(INode node) { private void removeInstance(InstanceDTO instance) {
instanceRepository.removeNode(node); client.getInstanceRepository().deleteInstance(InstanceId.of(instance.getId()));
refresh(); refresh();
} }
@Override @Override
public void refresh() { 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()); applySearch(searchField.getValue());
} }

View File

@ -58,19 +58,19 @@ public class NavPath extends HorizontalLayout{
return button; return button;
} }
public static NavPath rootNodes() { public static NavPath rootInstances() {
return new NavPath(new NavPath.Point("Nodes", "/nodes")); return new NavPath(new NavPath.Point("Instances", "/instances"));
} }
public static NavPath toNode(String nodeId) { public static NavPath toInstance(String instanceId) {
return new NavPath(new NavPath.Point("Nodes", "/nodes"), return new NavPath(new NavPath.Point("Instances", "/instances"),
new NavPath.Point(nodeId, "/nodes/" + nodeId)); new NavPath.Point(instanceId, "/instances/" + instanceId));
} }
public static NavPath toRoom(String nodeId, String roomId) { public static NavPath toRoom(String instanceId, String roomId) {
return new NavPath(new NavPath.Point("Nodes", "/nodes"), return new NavPath(new NavPath.Point("Instances", "/instances"),
new NavPath.Point(nodeId, "/nodes/" + nodeId), new NavPath.Point(instanceId, "/instances/" + instanceId),
new NavPath.Point(roomId, "/nodes/" + nodeId + "/rooms/" + roomId)); new NavPath.Point(roomId, "/instances/" + instanceId + "/rooms/" + roomId));
} }
private record Point(String name, String uri) {} private record Point(String name, String uri) {}

View File

@ -15,25 +15,27 @@ import com.vaadin.flow.component.textfield.Autocomplete;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.ComponentRenderer;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import ru.dragonestia.picker.api.model.node.NodeDefinition; import ru.dragonestia.picker.api.exception.InvalidIdentifierException;
import ru.dragonestia.picker.api.model.node.PickingMethod; import ru.dragonestia.picker.api.model.instance.InstanceId;
import ru.dragonestia.picker.api.repository.type.NodeIdentifier; 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; import java.util.function.Function;
public class RegisterNode extends Details { public class RegisterInstance extends Details {
private final Function<NodeDefinition, Response> onSubmit; private final Function<InstanceDTO, Response> onSubmit;
private final TextField identifierField; private final TextField identifierField;
private final RadioButtonGroup<PickingMethod> modeRadio; private final RadioButtonGroup<PickingMethod> modeRadio;
private final Checkbox persistField; private final Checkbox persistField;
public RegisterNode(Function<NodeDefinition, Response> onSubmit) { public RegisterInstance(Function<InstanceDTO, Response> onSubmit) {
super(new H2("Register node")); super(new H2("Register instance"));
this.onSubmit = onSubmit; this.onSubmit = onSubmit;
var layout = new VerticalLayout(); var layout = new VerticalLayout();
layout.add(identifierField = createNodeIdentifierField()); layout.add(identifierField = createInstanceIdentifierField());
layout.add(modeRadio = createModeRadio()); layout.add(modeRadio = createModeRadio());
layout.add(persistField = createPersistField()); layout.add(persistField = createPersistField());
layout.add(createSubmitButton()); layout.add(createSubmitButton());
@ -41,10 +43,10 @@ public class RegisterNode extends Details {
add(layout); add(layout);
} }
private TextField createNodeIdentifierField() { private TextField createInstanceIdentifierField() {
var field = new TextField("Identifier"); var field = new TextField("Identifier");
field.setMinWidth(20, Unit.REM); 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.setHelperText("The field can contain only lowercase letters, numbers and a dash character");
field.setPattern("^[a-z\\d-]+$"); field.setPattern("^[a-z\\d-]+$");
field.setRequired(true); field.setRequired(true);
@ -83,36 +85,61 @@ public class RegisterNode extends Details {
private @Nullable String validateForm(String identifier) { private @Nullable String validateForm(String identifier) {
if (identifier.isEmpty()) { 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; return null;
} }
private void onClick() { private void onClick() {
String nodeIdentifier = identifierField.getValue(); String instanceIdentifier = identifierField.getValue();
String error = null; String error = null;
if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) { if (identifierField.isInvalid() || (error = validateForm(instanceIdentifier)) != null) {
if (identifierField.isInvalid()) { if (identifierField.isInvalid()) {
error = "Invalid node id format"; error = "Invalid instance id format";
} }
Notifications.error(error); Notifications.error(error);
return; return;
} }
var node = new NodeDefinition(NodeIdentifier.of(nodeIdentifier)) var response = onSubmit.apply(new InstanceDTO() {
.setPickingMethod(modeRadio.getValue())
.setPersist(persistField.getValue()); @Override
var response = onSubmit.apply(node); 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(); clear();
if (response.error()) { if (response.error()) {
Notifications.error(response.reason()); Notifications.error(response.reason());
return; return;
} }
Notifications.success("Node was successfully registered"); Notifications.success("Instance was successfully registered");
} }
public record Response(boolean error, @Nullable String reason) {} public record Response(boolean error, @Nullable String reason) {}

View File

@ -11,29 +11,29 @@ import com.vaadin.flow.component.textfield.Autocomplete;
import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import ru.dragonestia.picker.api.model.node.INode; import ru.dragonestia.picker.api.model.instance.Instance;
import ru.dragonestia.picker.api.model.room.IRoom; import ru.dragonestia.picker.api.model.room.Room;
import ru.dragonestia.picker.api.model.room.RoomDefinition; import ru.dragonestia.picker.api.model.room.RoomId;
import ru.dragonestia.picker.api.repository.type.RoomIdentifier; import ru.dragonestia.picker.cp.util.Notifications;
import java.util.function.Function; import java.util.function.Function;
public class RegisterRoom extends Details { public class RegisterRoom extends Details {
private final INode node; private final Instance instance;
private final Function<RoomDefinition, Response> onSubmit; private final Function<Room, Response> onSubmit;
private final TextField identifierField; private final TextField identifierField;
private final TextArea payloadField; private final TextArea payloadField;
private final Checkbox lockedField; private final Checkbox lockedField;
private final Checkbox persistField; private final Checkbox persistField;
public RegisterRoom(INode node, Function<RoomDefinition, Response> onSubmit) { public RegisterRoom(Instance instance, Function<Room, Response> onSubmit) {
super(new H2("Register room")); super(new H2("Register room"));
this.node = node; this.instance = instance;
this.onSubmit = onSubmit; this.onSubmit = onSubmit;
var layout = new VerticalLayout(); var layout = new VerticalLayout();
layout.add(createNodeIdentifierField()); layout.add(createInstanceIdentifierField());
layout.add(identifierField = createRoomIdentifierField()); layout.add(identifierField = createRoomIdentifierField());
layout.add(payloadField = createPayloadField()); layout.add(payloadField = createPayloadField());
layout.add(lockedField = createLockedField()); layout.add(lockedField = createLockedField());
@ -43,10 +43,10 @@ public class RegisterRoom extends Details {
add(layout); add(layout);
} }
private TextField createNodeIdentifierField() { private TextField createInstanceIdentifierField() {
var field = new TextField("Node identifier"); var field = new TextField("Instance identifier");
field.setMinWidth(20, Unit.REM); field.setMinWidth(20, Unit.REM);
field.setValue(node.getIdentifier()); field.setValue(instance.id().getValue());
field.setReadOnly(true); field.setReadOnly(true);
return field; return field;
} }
@ -97,7 +97,7 @@ public class RegisterRoom extends Details {
private @Nullable String validateForm(String identifier) { private @Nullable String validateForm(String identifier) {
if (identifier.isEmpty()) { if (identifier.isEmpty()) {
return "Node identifier cannot be empty"; return "Instance identifier cannot be empty";
} }
return null; return null;
@ -116,13 +116,7 @@ public class RegisterRoom extends Details {
return; return;
} }
var room = new RoomDefinition(node.getIdentifierObject(), RoomIdentifier.of(roomId)) var response = onSubmit.apply(new Room(RoomId.of(roomId), instance.id(), -1, lockedField.getValue(), payloadField.getValue(), persistField.getValue()));
.setMaxSlots(IRoom.UNLIMITED_SLOTS)
.setPayload(payloadField.getValue())
.setPersist(persistField.getValue());
room.setLocked(lockedField.getValue());
var response = onSubmit.apply(room);
clear(); clear();
if (response.error()) { if (response.error()) {
Notifications.error(response.reason()); Notifications.error(response.reason());

View File

@ -15,27 +15,31 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.data.value.ValueChangeMode;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.api.model.node.INode; import ru.dragonestia.picker.api.impl.RoomPickerClient;
import ru.dragonestia.picker.api.model.room.RoomDetails; import ru.dragonestia.picker.api.model.instance.Instance;
import ru.dragonestia.picker.api.model.room.ShortResponseRoom; import ru.dragonestia.picker.api.model.instance.InstanceId;
import ru.dragonestia.picker.api.repository.RoomRepository; import ru.dragonestia.picker.api.model.room.RoomId;
import ru.dragonestia.picker.api.repository.query.room.GetAllRooms; 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; import java.util.List;
@Log4j2 @Log4j2
public class RoomList extends VerticalLayout implements RefreshableTable { public class RoomList extends VerticalLayout implements RefreshableTable {
private final INode node; private final Instance instance;
private final RoomRepository roomRepository; private final RoomPickerClient client;
private final Grid<ShortResponseRoom> roomsGrid; private final Grid<RoomDTO> roomsGrid;
private final TextField searchField; private final TextField searchField;
private List<ShortResponseRoom> cachedRooms; private List<RoomDTO> cachedRooms;
private final Span totalUsers = new Span(); private final Span totalEntities = new Span();
public RoomList(INode node, RoomRepository roomRepository) { public RoomList(Instance instance, RoomPickerClient client) {
this.node = node; this.instance = instance;
this.roomRepository = roomRepository; this.client = client;
add(new H2("Rooms")); add(new H2("Rooms"));
add(searchField = createSearchField()); add(searchField = createSearchField());
@ -58,39 +62,39 @@ public class RoomList extends VerticalLayout implements RefreshableTable {
var temp = input.trim(); var temp = input.trim();
roomsGrid.setItems(cachedRooms.stream() roomsGrid.setItems(cachedRooms.stream()
.filter(room -> room.getIdentifier().startsWith(temp)) .filter(room -> room.getId().startsWith(temp))
.toList()); .toList());
} }
private Grid<ShortResponseRoom> createGrid() { private Grid<RoomDTO> createGrid() {
var grid = new Grid<>(ShortResponseRoom.class, false); 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 -> { grid.addComponentColumn(room -> {
var result = new Span(); var result = new Span();
if (room.getMaxSlots() == -1) { if (room.getSlots() == -1) {
result.setText("Unlimited"); result.setText("Unlimited");
result.getElement().getThemeList().add("badge contrast"); result.getElement().getThemeList().add("badge contrast");
} else { } else {
result.setText(Integer.toString(room.getMaxSlots())); result.setText(Integer.toString(room.getSlots()));
} }
return result; return result;
}).setHeader("Slots").setComparator((room1, room2) -> { }).setHeader("Slots").setComparator((room1, room2) -> {
var r1 = room1.hasUnlimitedSlots()? Integer.MAX_VALUE : room1.getMaxSlots(); var r1 = room1.getSlots() == 1? Integer.MAX_VALUE : room1.getSlots();
var r2 = room2.hasUnlimitedSlots()? Integer.MAX_VALUE : room2.getMaxSlots(); var r2 = room2.getSlots() == 1? Integer.MAX_VALUE : room2.getSlots();
return Integer.compare(r1, r2); return Integer.compare(r1, r2);
}).setSortable(true).setTextAlign(ColumnTextAlign.CENTER); }).setSortable(true).setTextAlign(ColumnTextAlign.CENTER);
grid.addColumn(this::getUsers).setHeader("Users") grid.addColumn(RoomDTO::getCountEntities).setHeader("Entities")
.setComparator((room1, room2) -> Integer.compare(getUsers(room1), getUsers(room2))).setSortable(true) .setComparator(Comparator.comparingInt(RoomDTO::getCountEntities)).setSortable(true)
.setTextAlign(ColumnTextAlign.CENTER).setFooter(totalUsers); .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) -> { .setComparator((room1, room2) -> {
var p1 = UserList.getUsingPercentage(room1.getMaxSlots(), getUsers(room1)); var p1 = UsingSlots.getUsingPercentage(room1.getSlots(), room1.getCountEntities());
var p2 = UserList.getUsingPercentage(room2.getMaxSlots(), getUsers(room2)); var p2 = UsingSlots.getUsingPercentage(room2.getSlots(), room2.getCountEntities());
return Integer.compare(p1, p2); return Integer.compare(p1, p2);
}).setHeader("Occupancy").setTextAlign(ColumnTextAlign.CENTER); }).setHeader("Occupancy").setTextAlign(ColumnTextAlign.CENTER);
@ -114,7 +118,7 @@ public class RoomList extends VerticalLayout implements RefreshableTable {
return grid; return grid;
} }
private HorizontalLayout createManageButtons(ShortResponseRoom room) { private HorizontalLayout createManageButtons(RoomDTO room) {
var layout = new HorizontalLayout(JustifyContentMode.END); var layout = new HorizontalLayout(JustifyContentMode.END);
{ {
@ -134,15 +138,15 @@ public class RoomList extends VerticalLayout implements RefreshableTable {
return layout; return layout;
} }
private void clickDetailsButton(ShortResponseRoom room) { private void clickDetailsButton(RoomDTO room) {
getUI().ifPresent(ui -> { 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"); var dialog = new Dialog("Confirm room deletion");
dialog.add(new Html("<p>Confirm that you want to delete room. Enter <b><u>" + room.getIdentifier() + "</u></b> to field below and confirm.</p>")); dialog.add(new Html("<p>Confirm that you want to delete room. Enter <b><u>" + room.getId() + "</u></b> to field below and confirm.</p>"));
var inputField = new TextField(); var inputField = new TextField();
inputField.setWidth("100%"); inputField.setWidth("100%");
@ -152,13 +156,13 @@ public class RoomList extends VerticalLayout implements RefreshableTable {
var button = new Button("Confirm"); var button = new Button("Confirm");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> { button.addClickListener(event -> {
if (!room.getIdentifier().equals(inputField.getValue())) { if (!room.getId().equals(inputField.getValue())) {
Notifications.error("Invalid input"); Notifications.error("Invalid input");
return; return;
} }
removeRoom(room); removeRoom(room);
Notifications.success("Room <b>" + room.getIdentifier() + "</b> was successfully removed!"); Notifications.success("Room <b>" + room.getId() + "</b> was successfully removed!");
dialog.close(); dialog.close();
}); });
@ -174,31 +178,23 @@ public class RoomList extends VerticalLayout implements RefreshableTable {
dialog.open(); dialog.open();
} }
public void removeRoom(ShortResponseRoom room) { public void removeRoom(RoomDTO room) {
roomRepository.removeRoom(room); client.getRoomRepository().deleteRoom(InstanceId.of(room.getInstanceId()), RoomId.of(room.getId()));
refresh(); 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 @Override
public void refresh() { 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()); applySearch(searchField.getValue());
int users = 0; int entities = 0;
for (var room: cachedRooms) { for (var room: cachedRooms) {
users += getUsers(room); entities += room.getCountEntities();
} }
totalUsers.setText("Total users: " + users); totalEntities.setText("Total entities: " + entities);
} }
} }

View File

@ -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<IUser> usersGrid;
private final Span totalUsers = new Span();
private final Span occupancy = new Span();
private List<IUser> 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<IUser> createUsersGrid() {
var grid = new Grid<IUser>();
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()) + "%"));
}
}

View File

@ -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<RestTemplate> restTemplateSupplier(@Autowired RestTemplateBuilder builder) {
return builder::build;
}
}

View File

@ -3,11 +3,7 @@ package ru.dragonestia.picker.cp.config;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.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 @Configuration
public class RoomPickerConfig { public class RoomPickerConfig {
@ -15,25 +11,9 @@ public class RoomPickerConfig {
@Value("${ROOMPICKER_HOST_URL:http://localhost:8080}") @Value("${ROOMPICKER_HOST_URL:http://localhost:8080}")
private String serverUrl; private String serverUrl;
@Value("${ROOMPICKER_ADMIN_USERNAME:admin}")
private String adminUsername;
@Value("${ROOMPICKER_ADMIN_PASSWORD:qwerty123}")
private String adminPassword;
@ServerURL @ServerURL
@Bean @Bean
String severUrl() { String severUrl() {
return serverUrl; 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()));
}
} }

View File

@ -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);
}
}

View File

@ -7,7 +7,7 @@ import com.vaadin.flow.server.ErrorHandler;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.api.exception.ApiException; import ru.dragonestia.picker.api.exception.ApiException;
import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions; 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; import java.security.InvalidParameterException;
@ -21,30 +21,22 @@ public class ApplicationErrorHandler implements ErrorHandler {
return; return;
} }
if (errorEvent.getThrowable() instanceof ApiException ex) { if (errorEvent.getThrowable().getClass().getAnnotation(ApiException.class) != null) {
execute(() -> { execute(() -> Notifications.error(errorEvent.getThrowable().getMessage()));
Notifications.error(ex.getMessage());
});
return; return;
} }
if (errorEvent.getThrowable() instanceof InvalidParameterException ex) { if (errorEvent.getThrowable() instanceof InvalidParameterException ex) {
execute(() -> { execute(() -> Notifications.error(ex.getMessage()));
Notifications.error(ex.getMessage());
});
return; return;
} }
if (errorEvent.getThrowable() instanceof NotEnoughPermissions) { if (errorEvent.getThrowable() instanceof NotEnoughPermissions) {
execute(() -> { execute(() -> Notifications.error("Not enough permissions to this action"));
Notifications.error("Not enough permissions to this action");
});
return; return;
} }
execute(() -> { execute(() -> Notifications.error("Internal server error"));
Notifications.error("Internal server error");
});
log.throwing(errorEvent.getThrowable()); log.throwing(errorEvent.getThrowable());
} }

View File

@ -0,0 +1,3 @@
package ru.dragonestia.picker.cp.exception;
public class Unauthorized extends RuntimeException {}

View File

@ -3,10 +3,8 @@ package ru.dragonestia.picker.cp.listener;
import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener; import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.spring.annotation.SpringComponent; import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.cp.error.ApplicationErrorHandler; import ru.dragonestia.picker.cp.error.ApplicationErrorHandler;
@Log4j2
@SpringComponent @SpringComponent
public class VaadinEventListener implements VaadinServiceInitListener { public class VaadinEventListener implements VaadinServiceInitListener {

View File

@ -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<Permission> 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<Permission> 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<String> getPermissions() {
return original.getPermissions();
}
@Override
public boolean isLocked() {
return original.isLocked();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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("<h1><u>RoomPicker!</u></h1>"));
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"));
}
}

View File

@ -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("<span>Identifier: <b>" + node.getIdentifier() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + node.getPickingMethod().name() + "</b></span>"));
add(layout);
}
}

View File

@ -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);
}
}

View File

@ -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("<span>Node identifier: <b>" + room.getInstanceIdentifier() + "</b></span>"));
roomInfo.add(new Html("<span>Room identifier: <b>" + room.getIdentifier() + "</b></span>"));
roomInfo.add(new Html("<span>Slots: <b>" + (room.hasUnlimitedSlots()? "Unlimited" : room.getMaxSlots()) + "</b></span>"));
roomInfo.add(new Html("<span>Locked: <b>" + (room.isLocked()? "Yes" : "No") + "</b></span>"));
}
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<IUser> 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");
}
}
}

View File

@ -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<ShortResponseRoom> 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<ShortResponseRoom> createGrid() {
var grid = new Grid<ShortResponseRoom>();
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<ShortResponseRoom> cachedRooms = client.getUserRepository()
.findRoomsLinkedWithUser(FindRoomsLinkedWithUser.withAllDetails(user.getIdentifierObject()));
gridRooms.setItems(cachedRooms);
}
}

View File

@ -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<IUser> userGrid;
private final Span foundUsers;
private List<IUser> 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<IUser> createUserGrid() {
var grid = new Grid<IUser>();
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()));
}
}

View File

@ -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<InvalidInstanceIdentifierException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<InvalidInstanceIdentifierException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
init(NavPath.toNode(nodeId), "Error 400", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -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<InvalidRoomIdentifierException> {
@Override
public int setErrorParameter(BeforeEnterEvent event, ErrorParameter<InvalidRoomIdentifierException> 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;
}
}

View File

@ -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<InstanceNotFoundException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<InstanceNotFoundException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
init(NavPath.toNode(nodeId), "Error 404", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -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<RoomNotFoundException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<RoomNotFoundException> 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;
}
}

View File

@ -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<? extends InstanceDTO> all();
}

View File

@ -0,0 +1,8 @@
package ru.dragonestia.picker.cp.repository.dto;
public interface EntityDTO {
String getId();
int getCountRooms();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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<AllEntities> 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<Entity> entities;
}
@Getter
public static class Entity implements EntityDTO {
private String id;
private int countRooms;
}
}

View File

@ -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<Instance> allInstances;
public static GraphqlQuery<AllInstances> 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;
}
}

View File

@ -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<Room> allRooms;
public static GraphqlQuery<AllRooms> 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;
}
}

View File

@ -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<EntityData> 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;
}
}

View File

@ -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<EntityRooms> query(String entityId) {
return new GraphqlQuery<>(QUERY, EntityRooms.class, params -> {
params.put("entityId", entityId);
});
}
@Getter
public static class Entity {
private List<Room> 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;
}
}

View File

@ -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<Entity> searchEntity;
public static GraphqlQuery<SearchEntity> 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;
}
}

View File

@ -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<? extends InstanceDTO> all() {
return sessionService.getSession().getClient().getRestTemplate().executeGraphQL(AllInstances.query()).getAllInstances();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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.Html;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;

View File

@ -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<Permission, String> 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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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.AbstractField;
import com.vaadin.flow.component.Html; 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.H2;
import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Hr; 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.component.textfield.PasswordField;
import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.*;
import com.vaadin.flow.router.BeforeEnterObserver; import ru.dragonestia.picker.api.model.account.Account;
import com.vaadin.flow.router.PageTitle; import ru.dragonestia.picker.api.model.account.Permission;
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 ru.dragonestia.picker.cp.component.AccountList; import ru.dragonestia.picker.cp.component.AccountList;
import ru.dragonestia.picker.cp.component.Notifications; import ru.dragonestia.picker.cp.service.SessionService;
import ru.dragonestia.picker.cp.model.Permission; import ru.dragonestia.picker.cp.util.Notifications;
import ru.dragonestia.picker.cp.service.SecurityService; import ru.dragonestia.picker.cp.util.RouteParamExtractor;
import ru.dragonestia.picker.cp.util.RouteParamsExtractor; import ru.dragonestia.picker.cp.view.layout.MainLayout;
import java.util.ArrayList; import java.util.ArrayList;
@RequiredArgsConstructor @PageTitle("UserDetails details")
@RolesAllowed("ADMIN")
@PageTitle("Account details")
@Route(value = "/admin/accounts/:accountId", layout = MainLayout.class) @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 Account account;
private final RouteParamsExtractor paramsExtractor;
private RoomPickerClient client; public AccountView(SessionService sessionService, RouteParamExtractor paramsExtractor) {
private IAccount account; super(sessionService, paramsExtractor, Permission.ADMIN);
@PostConstruct
void postConstruct() {
client = securityService.getAuthenticatedAccount().getClient();
} }
@Override @Override
public void beforeEnter(BeforeEnterEvent event) { protected void preRender(RouteParameters routeParams) {
account = paramsExtractor.extractAccount(event); account = getParamsExtractor().account(routeParams);
init();
} }
private void init() { @Override
add(new H2("Account management")); protected void render() {
add(new Html("<span>Username: <b>%s</b></span>".formatted(account.getUsername()))); add(new H2("UserDetails management"));
add(new Html("<span>Entityname: <b>%s</b></span>".formatted(account.id())));
add(new Hr()); add(new Hr());
add(new H3("Change password")); add(new H3("Change password"));
@ -91,7 +76,7 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs
return; return;
} }
client.getAccountRepository().setPassword(account, pass); getClient().getAccountRepository().changePassword(account, pass);
Notifications.success("Password successfully changed!"); Notifications.success("Password successfully changed!");
newPassword.setValue(""); newPassword.setValue("");
confirmPassword.setValue(""); confirmPassword.setValue("");
@ -102,9 +87,11 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs
private void createEditPermissions() { private void createEditPermissions() {
var permissionsList = new ArrayList<AccountList.PermissionCheckBox>(); var permissionsList = new ArrayList<AccountList.PermissionCheckBox>();
for (var permission: Permission.Enum.values()) { for (var permission: Permission.values()) {
if (permission == Permission.ADMIN) continue;
var comp = new AccountList.PermissionCheckBox(permission); var comp = new AccountList.PermissionCheckBox(permission);
comp.setValue(account.getPermissions().contains(permission.name())); comp.setValue(account.permissions().contains(permission));
permissionsList.add(comp); permissionsList.add(comp);
add(comp); add(comp);
} }
@ -113,10 +100,9 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs
var permissions = permissionsList.stream() var permissions = permissionsList.stream()
.filter(AbstractField::getValue) .filter(AbstractField::getValue)
.map(AccountList.PermissionCheckBox::getOption) .map(AccountList.PermissionCheckBox::getOption)
.map(Enum::name)
.toList(); .toList();
client.getAccountRepository().setPermissions(account, permissions); getClient().getAccountRepository().setPermissions(account, permissions);
Notifications.success("Permissions successfully changed!"); Notifications.success("Permissions successfully changed!");
}); });
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@ -125,8 +111,8 @@ public class AccountDetailsPage extends VerticalLayout implements BeforeEnterObs
private Button createDeleteAccountButton() { private Button createDeleteAccountButton() {
var button = new Button("Delete this account", event -> { var button = new Button("Delete this account", event -> {
client.getAccountRepository().removeAccount(account); getClient().getAccountRepository().deleteAccount(account.id());
Notifications.warn("Account '%s' was deleted.".formatted(account.getUsername())); Notifications.warn("UserDetails '%s' was deleted.".formatted(account.id()));
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.navigate("/admin/accounts"); ui.navigate("/admin/accounts");

View File

@ -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()));
}
}

View File

@ -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());
}
}

View File

@ -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<RoomDTO> 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<RoomDTO> createGrid() {
var grid = new Grid<RoomDTO>();
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<RoomDTO> cachedRooms = getClient().getRestTemplate().executeGraphQL(EntityRooms.query(entity.getId())).getEntityById().getRooms().stream().map(entity -> (RoomDTO) entity).toList();
gridRooms.setItems(cachedRooms);
}
}

View File

@ -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("<span>Identifier: <b>" + instance.id() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + instance.method().name() + "</b></span>"));
add(layout);
}
}

View File

@ -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("<h1><u>RoomPicker!</u></h1>"));
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");
}
}
}

View File

@ -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("<span>Instance identifier: <b>" + room.instanceId() + "</b></span>"));
roomInfo.add(new Html("<span>Room identifier: <b>" + room.id() + "</b></span>"));
roomInfo.add(new Html("<span>Slots: <b>" + (room.slots() == -1? "Unlimited" : room.slots()) + "</b></span>"));
roomInfo.add(new Html("<span>Locked: <b>" + (room.locked()? "Yes" : "No") + "</b></span>"));
}
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<EntityId> 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");
}
}
}

View File

@ -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<EntityDTO> entityGrid;
private Span foundEntities;
private List<EntityDTO> 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<EntityDTO> createEntityGrid() {
var grid = new Grid<EntityDTO>();
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();
}
}

View File

@ -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();
}

View File

@ -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.Component;
import com.vaadin.flow.component.Html; 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.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.sidenav.SideNav; import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem; 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.api.repository.response.RoomPickerInfoResponse;
import ru.dragonestia.picker.cp.annotation.ServerURL; import ru.dragonestia.picker.cp.annotation.ServerURL;
import ru.dragonestia.picker.cp.model.Account; import ru.dragonestia.picker.cp.model.AccountSession;
import ru.dragonestia.picker.cp.service.SecurityService; 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 { public class MainLayout extends AppLayout {
private final SecurityService securityService; private final SessionService sessionService;
private final RoomPickerInfoResponse serverInfo;
private final String serverUrl; private final String serverUrl;
private final Account account; private final AccountSession session;
private final RoomPickerInfoResponse info;
private final boolean isAdmin; private final boolean isAdmin;
public MainLayout(SecurityService securityService, RoomPickerClient adminClient, @ServerURL String serverUrl) { public MainLayout(SessionService sessionService, @ServerURL String serverURL) {
this.securityService = securityService; this.sessionService = sessionService;
this.serverInfo = adminClient.getServerInfo(); this.serverUrl = serverURL;
this.serverUrl = serverUrl; this.session = sessionService.getSession();
account = securityService.getAuthenticatedAccount();
isAdmin = securityService.hasRole("ADMIN");
var toggle = new DrawerToggle(); if (session == null) {
var scroller = new Scroller(createSideNav()); info = null;
scroller.setWidth(100, Unit.PERCENTAGE); isAdmin = false;
} else {
info = session.getClient().getServerInfo();
isAdmin = session.getData().permissions().contains(Permission.ADMIN);
var navLayout = new VerticalLayout(createAccountButtons(), new Hr(), scroller); var toggle = new DrawerToggle();
navLayout.setPadding(false); var scroller = new Scroller(createSideNav());
scroller.setWidth(100, Unit.PERCENTAGE);
addToDrawer(navLayout); var navLayout = new VerticalLayout(createAccountButtons(), new Hr(), scroller);
addToNavbar(toggle, createLogo()); navLayout.setPadding(false);
addToDrawer(navLayout);
addToNavbar(toggle, createLogo());
}
} }
private Component createLogo() { private Component createLogo() {
@ -53,17 +62,17 @@ public class MainLayout extends AppLayout {
layout.setAlignItems(FlexComponent.Alignment.END); layout.setAlignItems(FlexComponent.Alignment.END);
layout.setPadding(true); layout.setPadding(true);
layout.add(new Html("<h2><u>RoomPicker!</u></h2>")); layout.add(new Html("<h2><u>RoomPicker!</u></h2>"));
layout.add(new Html("<sub>" + serverInfo.version() + "</sub>")); layout.add(new Html("<sub>" + info.version() + "</sub>"));
return layout; return layout;
} }
private Component createAccountButtons() { private Component createAccountButtons() {
var layout = new VerticalLayout(); var layout = new VerticalLayout();
var username = new Span(new Icon(isAdmin? VaadinIcon.USER_STAR : VaadinIcon.USER)); 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); 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); logoutButton.setWidth(100, Unit.PERCENTAGE);
layout.add(logoutButton); layout.add(logoutButton);
@ -72,10 +81,10 @@ public class MainLayout extends AppLayout {
private SideNav createSideNav() { private SideNav createSideNav() {
var nav = new SideNav(); var nav = new SideNav();
nav.addItem(new SideNavItem("Nodes list", NodesPage.class, VaadinIcon.FOLDER_O.create())); nav.addItem(new SideNavItem("Instances list", AllInstancesView.class, VaadinIcon.FOLDER_O.create()));
nav.addItem(new SideNavItem("Search users", UserSearchPage.class, VaadinIcon.SEARCH.create())); nav.addItem(new SideNavItem("Search entities", SearchEntityView.class, VaadinIcon.SEARCH.create()));
if (isAdmin) { 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("Documentation", "https://github.com/ScarletRedMan/RoomPicker", VaadinIcon.BOOK.create()));
nav.addItem(new SideNavItem("Swagger UI", serverUrl + "/api-docs-ui", VaadinIcon.CURLY_BRACKETS.create())); nav.addItem(new SideNavItem("Swagger UI", serverUrl + "/api-docs-ui", VaadinIcon.CURLY_BRACKETS.create()));

View File

@ -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;
import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.ParentLayout; import com.vaadin.flow.router.ParentLayout;
import ru.dragonestia.picker.cp.component.NavPath; 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) @ParentLayout(MainLayout.class)
public abstract class ErrorPlug extends VerticalLayout { public abstract class ErrorPlug extends VerticalLayout {
public void init(NavPath path, String title, String description) { public void init(String title, String description) {
add(path);
add(new H1(title)); add(new H1(title));
add(new Html("<p>" + description + "</p>")); add(new Html("<p>" + description + "</p>"));
} }

View File

@ -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<InvalidIdentifierException> {
@Override
public int setErrorParameter(BeforeEnterEvent event, ErrorParameter<InvalidIdentifierException> parameter) {
var ex = parameter.getException();
init("Error 400", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -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.H1;
import com.vaadin.flow.component.html.Paragraph; 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.ErrorParameter;
import com.vaadin.flow.router.HasErrorParameter; import com.vaadin.flow.router.HasErrorParameter;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.userdetails.User;
import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions; import ru.dragonestia.picker.api.impl.exception.NotEnoughPermissions;
public class NotEnoughPermissionsPlug extends ErrorPlug implements HasErrorParameter<NotEnoughPermissions> { public class NotEnoughPermissionsPlug extends ErrorPlug implements HasErrorParameter<NotEnoughPermissions> {

View File

@ -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<DoesNotExistsException> {
@Override
public int setErrorParameter(BeforeEnterEvent event, ErrorParameter<DoesNotExistsException> parameter) {
var ex = parameter.getException();
init("Error 404", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -59,20 +59,20 @@ public class UserMetricsAspect {
void onCreateNode(Instance instance) { void onCreateNode(Instance instance) {
var nodeId = instance.getId(); var nodeId = instance.getId();
var gauge = Gauge.builder("roompicker_node_users_total", () -> data.get(nodeId.getValue()).users()) var gauge = Gauge.builder("roompicker_node_users_total", () -> data.get(nodeId.getValue()).users())
.tag("nodeId", nodeId.getValue()) .tag("instanceId", nodeId.getValue())
.register(meterRegistry); .register(meterRegistry);
var counter = Counter.builder("roompicker_picks") var counter = Counter.builder("roompicker_picks")
.tag("nodeId", nodeId.getValue()) .tag("instanceId", nodeId.getValue())
.baseUnit("1s") .baseUnit("1s")
.register(meterRegistry); .register(meterRegistry);
var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId.getValue()).locked()) var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId.getValue()).locked())
.tag("nodeId", nodeId.getValue()) .tag("instanceId", nodeId.getValue())
.register(meterRegistry); .register(meterRegistry);
var roomsGauge = Gauge.builder("roompicker_rooms", () -> roomRepository.all(instance.getId()).size()) var roomsGauge = Gauge.builder("roompicker_rooms", () -> roomRepository.all(instance.getId()).size())
.tag("nodeId", nodeId.getValue()) .tag("instanceId", nodeId.getValue())
.register(meterRegistry); .register(meterRegistry);
data.put(nodeId.getValue(), new NodeData(gauge, new AtomicInteger(0), counter, new AtomicInteger(0), lockedGauge, roomsGauge)); data.put(nodeId.getValue(), new NodeData(gauge, new AtomicInteger(0), counter, new AtomicInteger(0), lockedGauge, roomsGauge));