refs #1558. Add client/subnet new UX modal
testing/ogcore-api/pipeline/head This commit looks good Details

pull/21/head
Manuel Aranda Rosales 2025-02-27 11:12:27 +01:00
parent a00dc7a59f
commit b3b3bf892d
19 changed files with 233 additions and 39 deletions

View File

@ -1,4 +1,31 @@
# Changelog
## [0.8.2] - 2025-03-04
### 🔹 Added
- Nueva funcionalidad para tener notificaciones en tiempo real. Instalación de bundle "Mercure".
- Nuevo endpoint "backup image". Integracion con ogRepository.
### ⚡ Changed
- Cambios en logs. Cambios en salida (stderror -> file.log)
---
## [0.8.1] - 2025-02-25
### 🐛 Fixed
- Corrección de bug en el deploy de imágenes
---
## [0.8.0] - 2025-01-10
### 🔹 Added
- Nuevos campos en "aulas" para la jerarquia en clientes.
- Nueva funcionalidad "imagen global". Integracion con ogRepository.
### ⚡ Changed
- Limpieza en campos "name" y "date" de ogLive. Es necesario parsear el campo "filename" para facilitar el uso al usuario en la web.
### 🐛 Fixed
- Corrección de bug que impedia borrar un cliente si tenia una traza enlazada.
-
---
## [0.7.3] - 2025-01-03
### 🔹 Added
@ -7,13 +34,9 @@
- Se agregó la funcionalidad de borrar imágenes. Integración con ogRepository.
- Se agregó el modo "TORRENT" y "UDPCAST" en el despliegue de imágenes.
### 🛠️ Fixed
### ⚡ Changed
- Refactorización del webhook de ogRepository.
### 🛑 Removed
---
## Formato de cambios:

View File

@ -20,7 +20,7 @@ services:
api_platform.filter.client.search:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact' } ]
arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', status: 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact', subnet.id: 'exact' } ]
tags: [ 'api_platform.filter' ]
api_platform.filter.client.exist:

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250225081416 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE network_settings ADD netiface VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE network_settings DROP netiface');
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250227095120 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD groups_view VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP groups_view');
}
}

View File

@ -46,7 +46,7 @@ class PostAction extends AbstractOgBootController
'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(),
'netmask' => $client->getOrganizationalUnit()->getNetworkSettings() ? $client->getOrganizationalUnit()->getNetworkSettings()->getNetmask() : '255.255.255.0',
'computer_name' => $client->getName(),
'netiface' => $client->getNetiface(),
'netiface' => $client->getNetiface() ? $client->getNetiface() : $client->getOrganizationalUnit()->getNetworkSettings()->getNetiface(),
'group' => $client->getOrganizationalUnit()->getName(),
'ogrepo' => $ogRepoIp,
'ogcore' => $this->ogCoreIP,

View File

@ -29,27 +29,54 @@ class PostHostAction extends AbstractOgDhcpController
*/
public function __invoke(SubnetAddHostInput $input, Subnet $subnet): JsonResponse
{
$client = $input->client;
$clients = $input->clients;
/** @var Client $clientEntity */
$clientEntity = $client->getEntity();
$success = [];
$errors = [];
$data = [
'host' => $clientEntity->getName(),
'macAddress' => strtolower($clientEntity->getMac()),
'address' => $clientEntity->getIp(),
];
foreach ($clients as $client) {
/** @var Client $clientEntity */
$clientEntity = $client->getEntity();
$params = [
'json' => $data
];
$data = [
'host' => $clientEntity->getName(),
'macAddress' => strtolower($clientEntity->getMac()),
'address' => $clientEntity->getIp(),
];
$content = $this->createRequest('POST', 'http://'.$this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$subnet->getServerId().'/hosts', $params);
$params = ['json' => $data];
$subnet->addClient($clientEntity);
$this->entityManager->persist($subnet);
$this->entityManager->flush();
try {
$content = $this->createRequest(
'POST',
'http://' . $this->ogDhcpApiUrl . '/ogdhcp/v1/subnets/' . $subnet->getServerId() . '/hosts',
$params
);
return new JsonResponse(data: $content, status: Response::HTTP_OK);
// Guardar resultado exitoso
$success[] = [
'client' => $clientEntity->getName(),
'response' => $content
];
// Persistir solo si la llamada fue exitosa
$subnet->addClient($clientEntity);
$this->entityManager->persist($subnet);
$this->entityManager->flush();
} catch (\Throwable $e) { // Capturar cualquier error sin interrumpir
$errors[] = [
'client' => $clientEntity->getName(),
'error' => $e->getMessage()
];
}
}
return new JsonResponse(
[
'success' => $success,
'errors' => $errors
],
empty($errors) ? Response::HTTP_OK : Response::HTTP_MULTI_STATUS
);
}
}

View File

@ -41,6 +41,9 @@ class NetworkSettingsInput
#[Groups(['organizational-unit:write'])]
public ?string $ntp = null;
#[Groups(['organizational-unit:write'])]
public ?string $netiface = null;
#[OrganizationalUnitP2PMode]
#[Groups(['organizational-unit:write'])]
public ?string $p2pMode = null;
@ -93,6 +96,7 @@ class NetworkSettingsInput
$this->netmask = $networkSettings->getNetmask();
$this->router = $networkSettings->getRouter();
$this->ntp = $networkSettings->getNtp();
$this->netiface = $networkSettings->getNetiface();
$this->p2pMode = $networkSettings->getP2pMode();
$this->p2pTime = $networkSettings->getP2pTime();
$this->mcastIp = $networkSettings->getMcastIp();
@ -130,6 +134,7 @@ class NetworkSettingsInput
$networkSettings->setDns($this->dns);
$networkSettings->setNetmask($this->netmask);
$networkSettings->setRouter($this->router);
$networkSettings->setNetiface($this->netiface);
$networkSettings->setNtp($this->ntp);
$networkSettings->setP2pMode($this->p2pMode);
$networkSettings->setP2pTime($this->p2pTime);

View File

@ -12,7 +12,10 @@ use Symfony\Component\Validator\Constraints as Assert;
final class SubnetAddHostInput
{
/**
* @var ClientOutput[]
*/
#[Assert\NotNull]
#[Groups(['subnet:write'])]
public ?ClientOutput $client = null;
public ?array $clients = [];
}

View File

@ -29,6 +29,9 @@ final class UserInput
#[Groups('user:write')]
public ?string $password = null;
#[Groups('user:write')]
public ?string $groupsView = null;
#[Assert\NotNull]
#[Groups('user:write')]
public ?bool $enabled = true;
@ -63,6 +66,7 @@ final class UserInput
$this->username = $user->getUsername();
$this->enabled= $user->isEnabled();
$this->groupsView = $user->getGroupsView();
if ($user->getUserGroups()) {
foreach ($user->getUserGroups() as $userGroup) {
@ -85,6 +89,7 @@ final class UserInput
$user->setUsername($this->username);
$user->setEnabled($this->enabled);
$user->setGroupsView($this->groupsView);
foreach ($this->userGroups as $userGroup) {
$userGroupsToAdd[] = $userGroup->getEntity();

View File

@ -30,6 +30,9 @@ final class NetworkSettingsOutput extends AbstractOutput
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $ntp = null;
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $netiface = null;
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $p2pMode = null;
@ -80,6 +83,7 @@ final class NetworkSettingsOutput extends AbstractOutput
$this->netmask = $networkSettings->getNetmask();
$this->router = $networkSettings->getRouter();
$this->ntp = $networkSettings->getNtp();
$this->netiface = $networkSettings->getNetiface();
$this->p2pMode = $networkSettings->getP2pMode();
$this->p2pTime = $networkSettings->getP2pTime();
$this->mcastIp = $networkSettings->getMcastIp();

View File

@ -38,7 +38,7 @@ final class OrganizationalUnitOutput extends AbstractOutput
#[Groups(['organizational-unit:read'])]
public ?self $parent = null;
#[Groups(['organizational-unit:read', 'organizational-unit:read:collection:short'])]
#[Groups(['organizational-unit:read', 'client:read', 'organizational-unit:read:collection:short'])]
public string $path;
#[Groups(['organizational-unit:read', "client:read"])]

View File

@ -23,6 +23,8 @@ final class UserOutput extends AbstractOutput
public array $allowedOrganizationalUnits;
#[Groups(['user:read'])]
public array $userGroups;
#[Groups(['user:read'])]
public string $groupsView;
#[Groups(['user:read'])]
public \DateTime $createdAt;
@ -37,6 +39,7 @@ final class UserOutput extends AbstractOutput
$this->username = $user->getUsername();
$this->roles = $user->getRoles();
$this->enabled = $user->isEnabled();
$this->groupsView = $user->getGroupsView();
$this->userGroups = $user->getUserGroups()->map(
fn(UserGroup $userGroup) => new UserGroupOutput($userGroup)

View File

@ -31,6 +31,9 @@ class NetworkSettings extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)]
private ?string $ntp = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $netiface = null;
#[ORM\Column(nullable: true)]
private ?int $p2pTime = null;
@ -161,6 +164,18 @@ class NetworkSettings extends AbstractEntity
return $this;
}
public function getNetiface(): ?string
{
return $this->netiface;
}
public function setNetiface(?string $netiface): static
{
$this->netiface = $netiface;
return $this;
}
public function getP2pTime(): ?int
{
return $this->p2pTime;

View File

@ -51,6 +51,9 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
private ?string $newPassword = null;
private ?string $repeatNewPassword = null;
#[ORM\Column(length: 255)]
private ?string $groupsView = null;
public function __construct()
{
@ -245,4 +248,16 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
return $this;
}
public function getGroupsView(): ?string
{
return $this->groupsView;
}
public function setGroupsView(string $groupsView): static
{
$this->groupsView = $groupsView;
return $this;
}
}

View File

@ -32,6 +32,7 @@ final class UserFactory extends ModelFactory
return [
'password' => $hash,
'roles' => [],
'groupsView' => 'card',
'username' => self::faker()->text(180),
];
}

View File

@ -19,6 +19,7 @@ class CustomLineFormatter extends LineFormatter
'component' => 'ogcore',
'params' => $record['context'],
'desc' => $record['message'],
'datetime' => $record['datetime']->format('Y-m-d H:i:s'),
];
return json_encode($output, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;

View File

@ -23,6 +23,7 @@ class JWTCreatedListener
$payload['username'] = $user->getUsername();
$payload['uuid'] = $user->getUuid();
$payload['roles'] = $user->getRoles();
$payload['groupsView'] = $user->getGroupsView();
$event->setData($payload);
}

View File

@ -16,23 +16,24 @@ class ClientRepository extends AbstractRepository
parent::__construct($registry, Client::class);
}
public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId): array
public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId, array $filters = []): array
{
$query = $this->getEntityManager()->createQuery(
$entityManager = $this->getEntityManager();
$query = $entityManager->createQuery(
'SELECT o.path
FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id'
)->setParameter('id', $organizationalUnitId);
$result = $query->getOneOrNullResult();
if (!$result) {
return [];
}
$path = $result['path'] . '/%';
$query = $this->getEntityManager()->createQuery(
$query = $entityManager->createQuery(
'SELECT o.id
FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id OR o.path LIKE :path'
@ -42,13 +43,32 @@ class ClientRepository extends AbstractRepository
$unitIds = array_column($query->getArrayResult(), 'id');
$query = $this->getEntityManager()->createQuery(
'SELECT c
FROM App\Entity\Client c
WHERE c.organizationalUnit IN (:unitIds)'
)
$qb = $entityManager->createQueryBuilder();
$qb->select('c')
->from(Client::class, 'c')
->where('c.organizationalUnit IN (:unitIds)')
->setParameter('unitIds', $unitIds);
return $query->getResult();
foreach ($filters as $key => $value) {
if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') {
continue;
}
if ($key === 'exists') {
foreach ($value as $field => $existsValue) {
if ($existsValue === 'true') {
$qb->andWhere("c.$field IS NOT NULL");
} elseif ($existsValue === 'false') {
$qb->andWhere("c.$field IS NULL");
}
}
} else {
$qb->andWhere("c.$key = :$key")->setParameter($key, $value);
}
}
return $qb->getQuery()->getResult();
}
}

View File

@ -39,10 +39,11 @@ readonly class ClientProvider implements ProviderInterface
public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
{
$organizationalUnitId = $context['filters']['organizationalUnit.id'] ?? null;
$filters = $context['filters'] ?? [];
if ($organizationalUnitId) {
$clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants((int) $organizationalUnitId);
if (isset($filters['organizationalUnit.id'])) {
$organizationalUnitId = (int) $filters['organizationalUnit.id'];
$clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters);
$items = new \ArrayObject();
foreach ($clients as $client) {
@ -50,12 +51,20 @@ readonly class ClientProvider implements ProviderInterface
}
return new TraversablePaginator($items, 1, count($clients), count($clients));
}
} else {
$paginator = $this->collectionProvider->provide($operation, $uriVariables, $context);
return $this->collectionProvider->provide($operation, $uriVariables, $context);
$items = new \ArrayObject();
foreach ($paginator->getIterator() as $item) {
$items[] = new ClientOutput($item);
}
return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems());
}
}
public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$item = $this->itemProvider->provide($operation, $uriVariables, $context);