Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PS-786 add notify admin to broadcast notifications #535

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions databox/api/config/routes/alchemy_notify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alchemy_notify:
resource:
path: '@AlchemyNotifyBundle/src/Controller/'
namespace: Alchemy\NotifyBundle\Controller
type: attribute
4 changes: 4 additions & 0 deletions databox/api/src/Controller/Admin/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Alchemy\AclBundle\Entity\AccessControlEntry;
use Alchemy\AdminBundle\Controller\AbstractAdminDashboardController;
use Alchemy\AuthBundle\Security\JwtUser;
use Alchemy\ConfiguratorBundle\Entity\ConfiguratorEntry;
use Alchemy\NotifyBundle\Model\Notification;
use Alchemy\StorageBundle\Entity\MultipartUpload;
use Alchemy\WebhookBundle\Entity\Webhook;
use Alchemy\WebhookBundle\Entity\WebhookLog;
Expand Down Expand Up @@ -128,6 +130,8 @@ public function configureMenuItems(): iterable
yield $this->createDevMenu();
yield MenuItem::subMenu('Webhook', 'fas fa-network-wired')->setSubItems($webhookSubMenu);

yield MenuItem::linkToRoute('Notification', 'fas fa-bell', 'alchemy_notify_admin_index');

yield MenuItem::linkToCrud('Global Config', 'fa fa-gear', ConfiguratorEntry::class);
}
}
5 changes: 5 additions & 0 deletions expose/api/config/routes/alchemy_notify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alchemy_notify:
resource:
path: '@AlchemyNotifyBundle/src/Controller/'
namespace: Alchemy\NotifyBundle\Controller
type: attribute
4 changes: 4 additions & 0 deletions lib/php/notify-bundle/src/AlchemyNotifyBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Alchemy\NotifyBundle;

use Alchemy\NotifyBundle\Command\BroadcastNotificationCommand;
use Alchemy\NotifyBundle\Command\TestNotificationCommand;
use Alchemy\NotifyBundle\Controller\NotificationController;
use Alchemy\NotifyBundle\Notification\MockNotifier;
use Alchemy\NotifyBundle\Notification\NotifierInterface;
use Alchemy\NotifyBundle\Notification\SymfonyNotifier;
Expand Down Expand Up @@ -87,6 +89,8 @@ public function loadExtension(array $config, ContainerConfigurator $container, C
$isTest = 'test' === $builder->getParameter('kernel.environment');

$services->alias(NotifierInterface::class, $config['notifier_service'] ?? ($isTest ? MockNotifier::class : SymfonyNotifier::class));
$services->set(BroadcastNotificationCommand::class);
$services->set(TestNotificationCommand::class);
$services->set(NotificationController::class);
}
}
45 changes: 45 additions & 0 deletions lib/php/notify-bundle/src/Command/BroadcastNotificationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Alchemy\NotifyBundle\Command;

use Alchemy\NotifyBundle\Notification\NotifierInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
name: 'alchemy:notify:broadcast',
description: 'Send notification to all users',
)]
class BroadcastNotificationCommand extends Command
{
public function __construct(
private readonly NotifierInterface $notifier,
) {
parent::__construct();
}

protected function configure(): void
{
parent::configure();

$this
->addArgument('notificationId', InputArgument::REQUIRED)
->addArgument('payload', InputArgument::OPTIONAL, 'JSON Payload', '{}')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->notifier->broadcast(
$input->getArgument('notificationId'),
json_decode($input->getArgument('payload') ?? '{}', true, 512, JSON_THROW_ON_ERROR)
);

return Command::SUCCESS;
}
}
68 changes: 68 additions & 0 deletions lib/php/notify-bundle/src/Controller/NotificationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Alchemy\NotifyBundle\Controller;

use Alchemy\NotifyBundle\Form\NotifyForm;
use Alchemy\NotifyBundle\Model\Notification;
use Alchemy\NotifyBundle\Notification\NotifierInterface;
use App\Entity\Core\Workspace;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN')]
#[Route(path: '/admin/notifications', name: 'alchemy_notify_admin_')]
class NotificationController extends AbstractController
{
public function __construct(
private NotifierInterface $notifier,
private EntityManagerInterface $em,
)
{
}

#[Route('/', name: 'index')]
public function index(Request $request): Response
{
$workspaces = $this->em->getRepository(Workspace::class)->findAll();

$form = $this->createForm(NotifyForm::class, null, [
'workspaces' => $workspaces,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var Notification $notification */
$notification = $form->getData();
$notificationId = 'basic';
$payload = [
'subject' => $notification->subject,
'content' => $notification->content,
];

if ($notification->topic) {
$this->notifier->notifyTopic(
$notification->topic,
null,
$notificationId,
$payload
);
} else {
$this->notifier->broadcast(
$notificationId,
$payload
);
}

$request->getSession()->getFlashBag()->add('success', 'Notification sent!');

return $this->redirect($request->getRequestUri());
}

return $this->render('@AlchemyNotify/admin/index.html.twig', [
'form' => $form->createView(),
]);
}
}
43 changes: 43 additions & 0 deletions lib/php/notify-bundle/src/Form/NotifyForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Alchemy\NotifyBundle\Form;

use Alchemy\NotifyBundle\Model\Notification;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class NotifyForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'All users' => null,
];
foreach ($options['workspaces'] as $workspace) {
$choices[$workspace->getName()] = 'ws:'.$workspace->getId();
}

$builder
->add('topic', ChoiceType::class, [
'choices' => $choices,
])
->add('subject')
->add('content', TextareaType::class)
->add('submit', SubmitType::class)
;
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Notification::class,
]);
$resolver->setRequired([
'workspaces',
]);
}
}
14 changes: 14 additions & 0 deletions lib/php/notify-bundle/src/Model/Notification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Alchemy\NotifyBundle\Model;

use Symfony\Component\Validator\Constraints as Assert;

class Notification
{
#[Assert\NotBlank]
public ?string $subject = null;
#[Assert\NotBlank]
public ?string $content = null;
public ?string $topic = null;
}
9 changes: 9 additions & 0 deletions lib/php/notify-bundle/src/Notification/MockNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ public function notifyUser(string $userId, string $notificationId, array $parame
];
}

public function broadcast(string $notificationId, array $parameters = []): void
{
$this->sentNotifications[] = [
'userId' => '*',
'notificationId' => $notificationId,
'parameters' => $parameters,
];
}

public function sendEmail(string $email, string $notificationId, array $parameters = []): void
{
$this->sentNotifications[] = [
Expand Down
5 changes: 5 additions & 0 deletions lib/php/notify-bundle/src/Notification/NotifierInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public function notifyUser(
array $parameters = [],
): void;

public function broadcast(
string $notificationId,
array $parameters = [],
): void;

public function sendEmail(
string $email,
string $notificationId,
Expand Down
7 changes: 6 additions & 1 deletion lib/php/notify-bundle/src/Notification/SymfonyNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ public function __construct(
public function notifyUser(string $userId, string $notificationId, array $parameters = []): void
{
$recipient = new NovuSubscriberRecipient($userId);
$this->sendNotification($recipient, $notificationId, $parameters, 'user');
$this->sendNotification($recipient, $notificationId, $parameters);
}

public function broadcast(string $notificationId, array $parameters = []): void
{
$this->novuClient->broadcast($notificationId, $parameters);
}

public function sendEmail(string $email, string $notificationId, array $parameters = []): void
Expand Down
21 changes: 21 additions & 0 deletions lib/php/notify-bundle/src/Service/NovuClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ public function notifyTopic(
]);
}


public function broadcast(
string $notificationId,
array $parameters = [],
array $options = [],
): void {
$data = [
'name' => $notificationId,
'payload' => empty($parameters) ? new \stdClass : $parameters,
];

$transactionId = $options['transactionId'] ?? null;
if (null !== $transactionId) {
$data['transactionId'] = $transactionId;
}

$this->request('POST', '/v1/events/trigger/broadcast', [
'json' => $data,
]);
}

private function request(string $method, string $url, array $options = []): ResponseInterface
{
return $this->clientExceptionListener->wrapClientRequest(function () use ($method, $url, $options): ResponseInterface {
Expand Down
12 changes: 12 additions & 0 deletions lib/php/notify-bundle/templates/admin/index.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% extends '@EasyAdmin/page/content.html.twig' %}

{% block content_title %}Notifications{% endblock %}

{% block page_actions %}
{% endblock %}

{% block main %}
<div style="max-width: 500px;">
{{ form(form) }}
</div>
{% endblock %}
49 changes: 49 additions & 0 deletions novu/bridge/app/novu/workflows/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {workflow} from "@novu/framework";
import {z} from "zod";
import {Button, render, Section, Text} from "@react-email/components";
import DefaultEmail, {styles} from "@/app/novu/emails/DefaultEmail";
import React from "react";

export const basic = workflow(
"basic",
async ({step, payload: {subject, content, url}}) => {
await step.inApp("In-App Step", async () => {
return {
subject,
body: content,
redirect: url ? {
url,
} : undefined,
};
});

await step.email("send-email", async () => {
return {
subject,
body: render(<DefaultEmail>
<Section>
<Text>
{content}
</Text>
{url ? <Button style={styles.button}>View</Button> : null}
</Section>
</DefaultEmail>
),
};
});
},
{
payloadSchema: z.object({
url: z
.string()
.optional()
.describe("The resource URL"),
subject: z
.string()
.describe("The message subject"),
content: z
.string()
.describe("The message content"),
})
},
);
1 change: 1 addition & 0 deletions novu/bridge/app/novu/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./uploader";
export * from "./databox";
export * from "./expose";
export * from "./basic";
5 changes: 5 additions & 0 deletions uploader/api/config/routes/alchemy_notify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
alchemy_notify:
resource:
path: '@AlchemyNotifyBundle/src/Controller/'
namespace: Alchemy\NotifyBundle\Controller
type: attribute
Loading