refs #1558. Add client/subnet new UX modal
testing/ogcore-api/pipeline/head This commit looks good
Details
testing/ogcore-api/pipeline/head This commit looks good
Details
parent
a00dc7a59f
commit
b3b3bf892d
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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 = [];
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"])]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ final class UserFactory extends ModelFactory
|
|||
return [
|
||||
'password' => $hash,
|
||||
'roles' => [],
|
||||
'groupsView' => 'card',
|
||||
'username' => self::faker()->text(180),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue