diff --git a/estela-api/api/errors.py b/estela-api/api/errors.py index 66328852..0c04cc45 100644 --- a/estela-api/api/errors.py +++ b/estela-api/api/errors.py @@ -15,3 +15,12 @@ PAGE_NOT_FOUND = "Page not found." INSUFFICIENT_PERMISSIONS = "You do not have the {} permissions to perform this action." USER_NOT_FOUND = "User not found." +UNABLE_CHANGE_PASSWORD = "Unable to change password." +ERROR_SENDING_VERIFICATION_EMAIL = ( + "Your user was created but there was an error sending" + " the verification email. Please try to log in later." +) +UNAUTHORIZED_PROFILE = "You are unauthorized to see this user profile." +SEND_EMAIL_LATER = ( + "There was an error sending the verification email. Please try again later." +) diff --git a/estela-api/api/mixins.py b/estela-api/api/mixins.py index 6d55892a..4b046070 100644 --- a/estela-api/api/mixins.py +++ b/estela-api/api/mixins.py @@ -5,6 +5,7 @@ from rest_framework.permissions import IsAuthenticated from api.permissions import IsProjectUser, IsAdminOrReadOnly +from core.models import Notification class APIPageNumberPagination(PageNumberPagination): @@ -19,3 +20,16 @@ class BaseViewSet(viewsets.GenericViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated, IsProjectUser, IsAdminOrReadOnly] pagination_class = APIPageNumberPagination + + +class NotificationsHandlerMixin: + def save_notification(self, user, message, project): + notification = Notification( + message=message, + user=user, + project=project, + ) + notification.save() + for _user in project.users.all(): + notification.users.add(_user, through_defaults={"seen": False}) + notification.save() diff --git a/estela-api/api/serializers/cronjob.py b/estela-api/api/serializers/cronjob.py index ae9049d2..122e8ac8 100644 --- a/estela-api/api/serializers/cronjob.py +++ b/estela-api/api/serializers/cronjob.py @@ -2,6 +2,8 @@ from rest_framework import serializers from api import errors + +from api.mixins import NotificationsHandlerMixin from api.serializers.job_specific import ( SpiderJobArgSerializer, SpiderJobEnvVarSerializer, @@ -110,7 +112,9 @@ def create(self, validated_data): return cronjob -class SpiderCronJobUpdateSerializer(serializers.ModelSerializer): +class SpiderCronJobUpdateSerializer( + serializers.ModelSerializer, NotificationsHandlerMixin +): def validate(self, attrs): attrs = super(SpiderCronJobUpdateSerializer, self).validate(attrs) if "schedule" in attrs and not croniter.is_valid(attrs.get("schedule")): @@ -131,37 +135,45 @@ class Meta: ) def update(self, instance, validated_data): + user = instance["user"] + instance = instance["object"] status = validated_data.get("status", "") schedule = validated_data.get("schedule", "") unique_collection = validated_data.get("unique_collection", False) data_status = validated_data.get("data_status", "") data_expiry_days = int(validated_data.get("data_expiry_days", 1)) name = instance.name + message = "" if "schedule" in validated_data: instance.schedule = schedule update_schedule(name, schedule) + message = f"changed the schedule of Scheduled-job-{instance.cjid}." if "status" in validated_data: instance.status = status if status == SpiderCronJob.ACTIVE_STATUS: enable_cronjob(name) + message = f"enabled Scheduled-job-{instance.cjid}." elif status == SpiderCronJob.DISABLED_STATUS: disable_cronjob(name) + message = f"disabled Scheduled-job-{instance.cjid}." if "unique_collection" in validated_data: instance.unique_collection = unique_collection if "data_status" in validated_data: if data_status == DataStatus.PERSISTENT_STATUS: instance.data_status = DataStatus.PERSISTENT_STATUS - elif data_status == DataStatus.PENDING_STATUS: + message = f"changed data persistence of Scheduled-job-{instance.cjid} to persistent." + elif data_status == DataStatus.PENDING_STATUS and data_expiry_days > 0: instance.data_status = DataStatus.PENDING_STATUS - if data_expiry_days < 1: - raise serializers.ValidationError( - {"error": errors.POSITIVE_SMALL_INTEGER_FIELD} - ) - else: - instance.data_expiry_days = data_expiry_days + instance.data_expiry_days = data_expiry_days + message = f"changed data persistence of Scheduled-job-{instance.cjid} to {data_expiry_days} days." else: raise serializers.ValidationError({"error": errors.INVALID_DATA_STATUS}) + self.save_notification( + user=user, + message=message, + project=instance.spider.project, + ) instance.save() return instance diff --git a/estela-api/api/serializers/notification.py b/estela-api/api/serializers/notification.py new file mode 100644 index 00000000..90ce3316 --- /dev/null +++ b/estela-api/api/serializers/notification.py @@ -0,0 +1,60 @@ +from rest_framework import serializers + +from api.serializers.project import ProjectDetailSerializer, UserDetailSerializer +from core.models import Notification, UserNotification + + +class NotificationSerializer(serializers.ModelSerializer): + user = UserDetailSerializer( + required=True, help_text="User who performed the action." + ) + project = ProjectDetailSerializer( + required=True, help_text="Project where the action was performed." + ) + + class Meta: + model = Notification + fields = ("nid", "message", "user", "project") + + +class UserNotificationSerializer(serializers.ModelSerializer): + id = serializers.IntegerField( + required=True, help_text="Unique user notification ID." + ) + notification = NotificationSerializer( + required=True, help_text="Notification to which the user is subscribed." + ) + + class Meta: + model = UserNotification + fields = ("id", "notification", "seen", "created") + + def to_representation(self, instance): + ret = super().to_representation(instance) + user = instance.notification.user + + is_superuser = user.is_superuser or user.is_staff + user_in_project = instance.notification.project.users.filter(id=user.id) + if is_superuser and not user_in_project: + ret["notification"]["user"]["username"] = "Bitmaker Cloud Admin" + else: + ret["notification"]["user"][ + "username" + ] = user.username + return ret + + +class UserNotificationUpdateSerializer(serializers.ModelSerializer): + seen = serializers.BooleanField( + required=False, + help_text="Whether the user has seen the notification.", + ) + + class Meta: + model = UserNotification + fields = ("id", "seen") + + def update(self, instance, validated_data): + instance.seen = validated_data.get("seen", instance.seen) + instance.save() + return instance diff --git a/estela-api/api/serializers/project.py b/estela-api/api/serializers/project.py index ab0f24e8..8b09f4fc 100644 --- a/estela-api/api/serializers/project.py +++ b/estela-api/api/serializers/project.py @@ -20,6 +20,20 @@ class Meta: fields = ["user", "permission"] +class ProjectDetailSerializer(serializers.ModelSerializer): + pid = serializers.UUIDField( + read_only=True, help_text="A UUID identifying this project." + ) + name = serializers.CharField(read_only=True, help_text="Project name.") + + class Meta: + model = Project + fields = ( + "pid", + "name", + ) + + class ProjectSerializer(serializers.ModelSerializer): pid = serializers.UUIDField( read_only=True, help_text="A UUID identifying this project." @@ -109,6 +123,9 @@ class ProjectUpdateSerializer(serializers.ModelSerializer): email = serializers.EmailField( write_only=True, required=False, help_text="Email address." ) + name = serializers.CharField( + write_only=True, required=False, help_text="Project name." + ) action = serializers.ChoiceField( write_only=True, choices=ACTION_CHOICES, diff --git a/estela-api/api/urls.py b/estela-api/api/urls.py index 42958705..7e38036b 100644 --- a/estela-api/api/urls.py +++ b/estela-api/api/urls.py @@ -1,13 +1,16 @@ from rest_framework import routers -from api.views import auth as auth_views -from api.views import cronjob as cronjob_views -from api.views import deploy as deploy_views -from api.views import job as job_views -from api.views import job_data as job_data_views -from api.views import project as project_views -from api.views import spider as spider_views -from api.views import stats as stats_views +from api.views import ( + project as project_views, + deploy as deploy_views, + spider as spider_views, + job as job_views, + auth as auth_views, + cronjob as cronjob_views, + job_data as job_data_views, + stats as stats_views, + notification as notification_views, +) router = routers.DefaultRouter(trailing_slash=False) router.register( @@ -15,6 +18,11 @@ viewset=project_views.ProjectViewSet, basename="project", ) +router.register( + prefix=r"notifications", + viewset=notification_views.UserNotificationViewSet, + basename="notification", +) router.register( prefix=r"projects/(?P[0-9a-z-]+)/deploys", viewset=deploy_views.DeployViewSet, diff --git a/estela-api/api/views/auth.py b/estela-api/api/views/auth.py index a14b52e6..1cb5ae5f 100644 --- a/estela-api/api/views/auth.py +++ b/estela-api/api/views/auth.py @@ -17,6 +17,7 @@ from rest_framework.exceptions import MethodNotAllowed, PermissionDenied from rest_framework.response import Response +from api import errors from api.exceptions import EmailServiceError, UserNotFoundError from api.permissions import IsProfileUser from api.serializers.auth import ( @@ -85,11 +86,7 @@ def register(self, request, *args, **kwargs): try: send_verification_email(user, request) except Exception: - raise EmailServiceError( - { - "error": "Your user was created but there was an error sending the verification email. Please try to log in later." - } - ) + raise EmailServiceError({"error": errors.ERROR_SENDING_VERIFICATION_EMAIL}) token, _ = Token.objects.get_or_create(user=user) return Response(TokenSerializer(token).data) @@ -161,9 +158,7 @@ def retrieve(self, request, *args, **kwargs): ) if user != requested_user: return Response( - data={ - "error": "Unauthorized to see this profile, you are allowed to see only your profile." - }, + data={"error": errors.UNAUTHORIZED_PROFILE}, status=status.HTTP_401_UNAUTHORIZED, ) @@ -263,11 +258,7 @@ def request(self, request, *args, **kwargs): try: send_change_password_email(user) except Exception: - raise EmailServiceError( - { - "error": "There was an error sending the password reset email. Please try again later." - } - ) + raise EmailServiceError({"error": errors.SEND_EMAIL_LATER}) token, _ = Token.objects.get_or_create(user=user) return Response(TokenSerializer(token).data) diff --git a/estela-api/api/views/cronjob.py b/estela-api/api/views/cronjob.py index 84ad46ae..5ed5e751 100644 --- a/estela-api/api/views/cronjob.py +++ b/estela-api/api/views/cronjob.py @@ -8,18 +8,19 @@ from rest_framework.response import Response from api.filters import SpiderCronJobFilter -from api.mixins import BaseViewSet +from api.mixins import BaseViewSet, NotificationsHandlerMixin from api.serializers.cronjob import ( SpiderCronJobCreateSerializer, SpiderCronJobSerializer, SpiderCronJobUpdateSerializer, ) from core.cronjob import create_cronjob, disable_cronjob, run_cronjob_once -from core.models import DataStatus, Spider, SpiderCronJob +from core.models import DataStatus, Spider, SpiderCronJob, Project class SpiderCronJobViewSet( BaseViewSet, + NotificationsHandlerMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, @@ -93,6 +94,15 @@ def create(self, request, *args, **kwargs): cronjob.schedule, data_expiry_days=data_expiry_days, ) + + # Send notification action + project = get_object_or_404(Project, pid=self.kwargs["pid"]) + self.save_notification( + user=request.user, + message=f"scheduled a new Scheduled-job-{cronjob.cjid} for spider {spider.name}.", + project=project, + ) + headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers @@ -105,6 +115,7 @@ def create(self, request, *args, **kwargs): def update(self, request, *args, **kwargs): partial = kwargs.pop("partial", False) instance = self.get_object() + instance = {"object": self.get_object(), "user": request.user} serializer = SpiderCronJobUpdateSerializer( instance, data=request.data, partial=partial ) diff --git a/estela-api/api/views/deploy.py b/estela-api/api/views/deploy.py index 882d09a8..9d5ff3cf 100644 --- a/estela-api/api/views/deploy.py +++ b/estela-api/api/views/deploy.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.exceptions import ParseError, APIException, PermissionDenied -from api.mixins import BaseViewSet +from api.mixins import BaseViewSet, NotificationsHandlerMixin from api.serializers.deploy import ( DeploySerializer, DeployCreateSerializer, @@ -19,6 +19,7 @@ class DeployViewSet( BaseViewSet, + NotificationsHandlerMixin, viewsets.ModelViewSet, ): model_class = Deploy @@ -91,5 +92,13 @@ def update(self, request, *args, **kwargs): if getattr(instance, "_prefetched_objects_cache", None): instance._prefetched_objects_cache = {} + # Send action notification + project = get_object_or_404(Project, pid=self.kwargs["pid"]) + self.save_notification( + user=instance.user, + message=f"made a new Deploy #{serializer.data['did']}.", + project=project, + ) + headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_200_OK, headers=headers) diff --git a/estela-api/api/views/job.py b/estela-api/api/views/job.py index 14414d4e..5907df8f 100644 --- a/estela-api/api/views/job.py +++ b/estela-api/api/views/job.py @@ -1,4 +1,3 @@ -from datetime import date, timedelta from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend from drf_yasg.utils import swagger_auto_schema @@ -8,18 +7,19 @@ from rest_framework.exceptions import ParseError from api.filters import SpiderJobFilter -from api.mixins import BaseViewSet +from api.mixins import BaseViewSet, NotificationsHandlerMixin from api.serializers.job import ( SpiderJobSerializer, SpiderJobCreateSerializer, SpiderJobUpdateSerializer, ) from config.job_manager import job_manager -from core.models import DataStatus, Spider, SpiderJob +from core.models import DataStatus, Spider, SpiderJob, Project class SpiderJobViewSet( BaseViewSet, + NotificationsHandlerMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, @@ -138,6 +138,14 @@ def create(self, request, *args, **kwargs): data_expiry_days=data_expiry_days, ) + # Send action notification + project = get_object_or_404(Project, pid=self.kwargs["pid"]) + self.save_notification( + user=request.user, + message=f"run a new job for spider {spider.name}.", + project=project, + ) + headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers diff --git a/estela-api/api/views/notification.py b/estela-api/api/views/notification.py new file mode 100644 index 00000000..e80af841 --- /dev/null +++ b/estela-api/api/views/notification.py @@ -0,0 +1,39 @@ +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status, viewsets +from rest_framework.response import Response + +from api.mixins import BaseViewSet +from api.serializers.notification import ( + UserNotificationSerializer, + UserNotificationUpdateSerializer, +) +from core.models import UserNotification + + +class UserNotificationViewSet(BaseViewSet, viewsets.ModelViewSet): + model_class = UserNotification + serializer_class = UserNotificationSerializer + queryset = UserNotification.objects.all() + + def get_queryset(self): + if self.request is None: + return UserNotification.objects.none() + return UserNotification.objects.filter(user=self.request.user) + + @swagger_auto_schema( + request_body=UserNotificationUpdateSerializer, + responses={status.HTTP_200_OK: UserNotificationUpdateSerializer()}, + ) + def update(self, request, *args, **kwargs): + partial = kwargs.pop("partial", False) + instance = self.get_object() + serializer = UserNotificationUpdateSerializer( + instance, data=request.data, partial=partial + ) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + if getattr(instance, "_prefetched_objects_cache", None): + instance._prefetched_objects_cache = {} + + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/estela-api/api/views/project.py b/estela-api/api/views/project.py index c691604a..cbe51495 100644 --- a/estela-api/api/views/project.py +++ b/estela-api/api/views/project.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from django.shortcuts import get_object_or_404 from django.core.paginator import Paginator from drf_yasg import openapi @@ -9,9 +10,9 @@ from rest_framework.response import Response from api import errors -from api.mixins import BaseViewSet -from api.serializers.cronjob import ProjectCronJobSerializer, SpiderCronJobSerializer +from api.mixins import BaseViewSet, NotificationsHandlerMixin from api.serializers.job import ProjectJobSerializer, SpiderJobSerializer +from api.serializers.cronjob import ProjectCronJobSerializer, SpiderCronJobSerializer from api.serializers.project import ( ProjectSerializer, ProjectUpdateSerializer, @@ -30,7 +31,7 @@ ) -class ProjectViewSet(BaseViewSet, viewsets.ModelViewSet): +class ProjectViewSet(BaseViewSet, NotificationsHandlerMixin, viewsets.ModelViewSet): model_class = Project queryset = Project.objects.all() serializer_class = ProjectSerializer @@ -70,6 +71,11 @@ def perform_create(self, serializer): requests_data_size=0, logs_data_size=0, ) + self.save_notification( + user=self.request.user, + message=f"created project {instance.name}.", + project=instance, + ) @swagger_auto_schema( request_body=ProjectUpdateSerializer, @@ -89,25 +95,32 @@ def update(self, request, *args, **kwargs): permission = serializer.validated_data.pop("permission", "") data_status = serializer.validated_data.pop("data_status", "") data_expiry_days = serializer.validated_data.pop("data_expiry_days", 0) + message = "" if name: + old_name = instance.name instance.name = name + message = f"renamed project {old_name} ({instance.pid}) to {name}." - if user_email and user_email != request.user.email: - if not ( - request.user.permission_set.get(project=instance).permission + user = request.user + is_superuser = user.is_superuser or user.is_staff + if user_email and (is_superuser or user_email != user.email): + if not is_superuser and not ( + user.permission_set.get(project=instance).permission in [Permission.ADMIN_PERMISSION, Permission.OWNER_PERMISSION] ): raise PermissionDenied( {"permission": "You do not have permission to do this."} ) - user = User.objects.filter(email=user_email) - if not user: + affected_user = User.objects.filter(email=user_email) + if not affected_user: raise NotFound({"email": "User does not exist."}) - user = user.get() - existing_permission = user.permission_set.filter(project=instance).first() + affected_user = affected_user.get() + existing_permission = affected_user.permission_set.filter( + project=instance + ).first() if ( existing_permission and existing_permission.permission == Permission.OWNER_PERMISSION @@ -117,19 +130,38 @@ def update(self, request, *args, **kwargs): ) if action == "add": - instance.users.add(user, through_defaults={"permission": permission}) + instance.users.add( + affected_user, through_defaults={"permission": permission} + ) + message = f"added {user_email}." elif action == "remove": - instance.users.remove(user) + instance.users.remove(affected_user) + message = f"removed {user_email}." elif action == "update": - instance.users.remove(user) - instance.users.add(user, through_defaults={"permission": permission}) + instance.users.remove(affected_user) + instance.users.add( + affected_user, through_defaults={"permission": permission} + ) + message = f"updated {user_email}'s permissions to {permission}." else: raise ParseError({"error": "Action not supported."}) if data_status: - instance.data_status = data_status - if data_status == DataStatus.PENDING_STATUS and data_expiry_days > 0: + if data_status == DataStatus.PERSISTENT_STATUS: + instance.data_status = DataStatus.PERSISTENT_STATUS + message = "changed data persistence to persistent." + elif data_status == DataStatus.PENDING_STATUS and data_expiry_days > 0: + instance.data_status = DataStatus.PENDING_STATUS instance.data_expiry_days = data_expiry_days + message = f"changed data persistence to {data_expiry_days} days." + else: + raise ParseError({"error": errors.INVALID_DATA_STATUS}) + + self.save_notification( + user=self.request.user, + message=message, + project=instance, + ) serializer.save() headers = self.get_success_headers(serializer.data) @@ -140,6 +172,12 @@ def update(self, request, *args, **kwargs): ) def destroy(self, request, *args, **kwargs): instance = self.get_object() + project = get_object_or_404(Project, pid=self.kwargs["pid"]) + self.save_notification( + user=self.request.user, + message=f"deleted project {instance.name} ({instance.pid}).", + project=project, + ) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/estela-api/core/migrations/0029_notification.py b/estela-api/core/migrations/0029_notification.py new file mode 100644 index 00000000..c11d3ed5 --- /dev/null +++ b/estela-api/core/migrations/0029_notification.py @@ -0,0 +1,107 @@ +# Generated by Django 3.1.14 on 2023-06-12 04:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0028_remove_job_statuses"), + ] + + operations = [ + migrations.CreateModel( + name="Notification", + fields=[ + ( + "nid", + models.AutoField( + help_text="A unique integer value identifying each notification", + primary_key=True, + serialize=False, + ), + ), + ( + "message", + models.CharField( + help_text="Notification message.", max_length=1000 + ), + ), + ( + "project", + models.ForeignKey( + help_text="Project where the notification belongs", + on_delete=django.db.models.deletion.CASCADE, + to="core.project", + ), + ), + ( + "user", + models.ForeignKey( + help_text="User who performed the action on this notification.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="UserNotification", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "seen", + models.BooleanField( + default=False, help_text="Whether the notification was seen." + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="Date when the notification was sent.", + ), + ), + ( + "notification", + models.ForeignKey( + help_text="Notification that the user received.", + on_delete=django.db.models.deletion.CASCADE, + to="core.notification", + ), + ), + ( + "user", + models.ForeignKey( + help_text="Related user.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created"], + }, + ), + migrations.AddField( + model_name="notification", + name="users", + field=models.ManyToManyField( + help_text="Users that received this notification.", + related_name="notifications", + through="core.UserNotification", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/estela-api/core/models.py b/estela-api/core/models.py index 236c6cbe..585bff2a 100644 --- a/estela-api/core/models.py +++ b/estela-api/core/models.py @@ -410,3 +410,51 @@ class UsageRecord(models.Model): class Meta: ordering = ["-created_at"] + + +class Notification(models.Model): + nid = models.AutoField( + primary_key=True, + help_text="A unique integer value identifying each notification", + ) + message = models.CharField(max_length=1000, help_text="Notification message.") + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + help_text="Project where the notification belongs", + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="User who performed the action on this notification.", + ) + users = models.ManyToManyField( + User, + through="UserNotification", + related_name="notifications", + help_text="Users that received this notification.", + ) + + +class UserNotification(models.Model): + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text="Related user.", + ) + notification = models.ForeignKey( + Notification, + on_delete=models.CASCADE, + help_text="Notification that the user received.", + ) + seen = models.BooleanField( + default=False, help_text="Whether the notification was seen." + ) + created = models.DateTimeField( + auto_now_add=True, + editable=False, + help_text="Date when the notification was sent.", + ) + + class Meta: + ordering = ["-created"] diff --git a/estela-api/docs/api.yaml b/estela-api/docs/api.yaml index 7fd7453e..d3f9aa4f 100644 --- a/estela-api/docs/api.yaml +++ b/estela-api/docs/api.yaml @@ -264,6 +264,122 @@ paths: tags: - api parameters: [] + /api/notifications: + get: + operationId: api_notifications_list + description: '' + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/UserNotification' + tags: + - api + post: + operationId: api_notifications_create + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserNotification' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/UserNotification' + tags: + - api + parameters: [] + /api/notifications/{id}: + get: + operationId: api_notifications_read + description: '' + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserNotification' + tags: + - api + put: + operationId: api_notifications_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserNotificationUpdate' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserNotificationUpdate' + tags: + - api + patch: + operationId: api_notifications_partial_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/UserNotification' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/UserNotification' + tags: + - api + delete: + operationId: api_notifications_delete + description: '' + parameters: [] + responses: + '204': + description: '' + tags: + - api + parameters: + - name: id + in: path + description: A unique integer value identifying this user notification. + required: true + type: integer /api/projects: get: operationId: api_projects_list @@ -1386,6 +1502,78 @@ definitions: type: string maxLength: 128 minLength: 1 + ProjectDetail: + description: Project where the action was performed. + type: object + properties: + pid: + title: Pid + description: A UUID identifying this project. + type: string + format: uuid + readOnly: true + name: + title: Name + description: Project name. + type: string + readOnly: true + minLength: 1 + Notification: + description: Notification to which the user is subscribed. + required: + - message + - user + - project + type: object + properties: + nid: + title: Nid + description: A unique integer value identifying each notification + type: integer + readOnly: true + message: + title: Message + description: Notification message. + type: string + maxLength: 1000 + minLength: 1 + user: + $ref: '#/definitions/UserDetail' + project: + $ref: '#/definitions/ProjectDetail' + UserNotification: + required: + - id + - notification + type: object + properties: + id: + title: Id + description: Unique user notification ID. + type: integer + notification: + $ref: '#/definitions/Notification' + seen: + title: Seen + description: Whether the notification was seen. + type: boolean + created: + title: Created + description: Date when the notification was sent. + type: string + format: date-time + readOnly: true + UserNotificationUpdate: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + seen: + title: Seen + description: Whether the user has seen the notification. + type: boolean Permission: description: Users with permissions on this project. type: object @@ -1455,8 +1643,6 @@ definitions: maximum: 65535 minimum: 0 ProjectUpdate: - required: - - name type: object properties: pid: @@ -1467,9 +1653,8 @@ definitions: readOnly: true name: title: Name - description: Project's name. + description: Project name. type: string - maxLength: 1000 minLength: 1 users: description: Affected users. @@ -2124,8 +2309,6 @@ definitions: - WAITING - RUNNING - STOPPED - - INCOMPLETE - - CANCELLED - COMPLETED - ERROR lifespan: diff --git a/estela-web/src/assets/icons/ellipse.svg b/estela-web/src/assets/icons/ellipse.svg index 7a6fa84f..a69795be 100644 --- a/estela-web/src/assets/icons/ellipse.svg +++ b/estela-web/src/assets/icons/ellipse.svg @@ -1,3 +1,3 @@ - - + + diff --git a/estela-web/src/pages/NotificationsInboxPage/index.tsx b/estela-web/src/pages/NotificationsInboxPage/index.tsx index 045702ae..b79d85a6 100644 --- a/estela-web/src/pages/NotificationsInboxPage/index.tsx +++ b/estela-web/src/pages/NotificationsInboxPage/index.tsx @@ -1,17 +1,143 @@ -import React, { Component } from "react"; -import { Layout } from "antd"; - +import React, { Component, ReactElement } from "react"; +import { Layout, Badge, Pagination } from "antd"; +import { + AuthService, + ApiService, + ApiNotificationsListRequest, + UserNotification, + ApiNotificationsUpdateRequest, +} from "../../services"; +import { Spin, PaginationItem } from "../../shared"; +import Circle from "../../assets/icons/ellipse.svg"; +import FolderDotted from "../../assets/icons/folderDotted.svg"; import "./styles.scss"; -import { NotificationsList } from "../../shared"; +import { convertDateToString } from "../../utils"; + +const { Content } = Layout; + +interface NotificationInboxState { + notifications: UserNotification[]; + loaded: boolean; + count: number; + current: number; +} + +export class NotificationsInboxPage extends Component { + PAGE_SIZE = 10; + state: NotificationInboxState = { + notifications: [], + loaded: false, + count: 0, + current: 1, + }; + + apiService = ApiService(); + + async componentDidMount(): Promise { + this.getNotifications(1); + } + + getNotifications = async (page: number): Promise => { + const requestParams: ApiNotificationsListRequest = { + pageSize: this.PAGE_SIZE, + page: page, + }; + this.apiService.apiNotificationsList(requestParams).then((response) => { + this.setState({ notifications: response.results, loaded: true, count: response.count, current: page }); + }); + }; + + onPageChange = async (page: number): Promise => { + await this.getNotifications(page); + }; + + changeNotificationStatus(id: number): void { + const notifications = this.state.notifications; + const index = notifications.findIndex((user_notification) => user_notification.id == id); + if (notifications[index].seen) return; + + const requestData = { + seen: true, + }; + const requestParams: ApiNotificationsUpdateRequest = { + id: id, + data: requestData, + }; + + notifications[index].seen = true; + this.setState({ notifications: notifications }); + this.apiService.apiNotificationsUpdate(requestParams).then((response) => { + notifications[index].seen = response.seen; + this.setState({ notifications: notifications }); + }); + } + + emptyNotification = (): ReactElement => ( + + +

No projects yet.

+ + ); -export class NotificationsInboxPage extends Component { render(): JSX.Element { + const { loaded, notifications, count, current } = this.state; return ( - -

Inbox

- - - + + +

Inbox

+ {loaded ? ( + + {notifications.length == 0 && this.emptyNotification()} + {notifications.map((user_notification) => ( +
this.changeNotificationStatus(user_notification.id)} + className="py-2 px-3 flex hover:bg-estela-blue-low hover:text-estela-blue-full rounded-md" + style={{ cursor: "pointer" }} + key={user_notification.id} + > + {!user_notification.seen ? ( + } + > + ) : ( +
+ )} +
+ + {convertDateToString(user_notification.created)}:  + {user_notification.notification.user.email == AuthService.getUserEmail() + ? "You" + : user_notification.notification.user.username} + + {AuthService.getUserEmail() == user_notification.notification.user.email + ? " have " + : " has "} + {user_notification.notification.message} Project:  + + {user_notification.notification.project.name} ( + {user_notification.notification.project.pid}) + +

+ {user_notification.createdAt?.toDateString()} +

+
+
+ ))} + +
+ ) : ( + + )} +
); } diff --git a/estela-web/src/pages/NotificationsSettingsPage/index.tsx b/estela-web/src/pages/NotificationsSettingsPage/index.tsx index db58fd7a..dadc4a22 100644 --- a/estela-web/src/pages/NotificationsSettingsPage/index.tsx +++ b/estela-web/src/pages/NotificationsSettingsPage/index.tsx @@ -3,55 +3,45 @@ import { Layout, Space, Row } from "antd"; import "./styles.scss"; -export class NotificationsSettingsPage extends Component { - updateNotifications = (): void => { - console.log(""); - }; +const { Content } = Layout; +export class NotificationsSettingsPage extends Component { render(): JSX.Element { return ( - -

Notifications settings

- - - - - - News and Updates - - News about Estela and feature updates. - - - - - - - - - Reply to Personal Email - - Reply Estela notifications to my personal email scraper202122@domain.com. - - - - - + + +

Notifications settings

+ + + + +
+

News and Updates

+

+ News about Estela and feature updates. +

+
+
+
+ + + +
+

Reply to Personal Email

+

+ Reply Estela notifications to my personal email scraper202122@domain.com. +

+
+
+
+
+
); } diff --git a/estela-web/src/pages/ProjectDashboardPage/index.tsx b/estela-web/src/pages/ProjectDashboardPage/index.tsx index 7792db9f..e3e8c99c 100644 --- a/estela-web/src/pages/ProjectDashboardPage/index.tsx +++ b/estela-web/src/pages/ProjectDashboardPage/index.tsx @@ -69,11 +69,13 @@ export class ProjectDashboardPage extends Component { const requestParams: ApiProjectsReadRequest = { pid: this.projectId }; this.apiService.apiProjectsRead(requestParams).then( (response: Project) => { + if (!this.mounted) return; this.setState({ name: response.name }); const { updateRole } = this.context as UserContextProps; const userRole = AuthService.getUserRole(); @@ -90,8 +92,13 @@ export class ProjectDashboardPage extends Component => { await this.apiService.apiProjectsCurrentUsage({ pid: this.projectId }).then((response: ProjectUsage) => { + if (!this.mounted) return; const time = parseFloat(response.processingTime ?? "0"); this.setState({ projectUseLoaded: true, @@ -140,6 +147,25 @@ export class ProjectDashboardPage extends Component => { + const params: ApiStatsJobsStatsRequest = { + pid: this.projectId, + data: jobsMetadata, + }; + await this.apiService.apiStatsJobsStats(params).then((response: GetJobsStats[]) => { + if (!this.mounted) return; + const { jobsDateStats, loadedJobsDateStats } = this.state; + const newLoadedJobsDateStats = [...loadedJobsDateStats]; + newLoadedJobsDateStats[index] = true; + const newJobsDateStats = [...jobsDateStats]; + newJobsDateStats[index] = response; + this.setState({ + jobsDateStats: [...newJobsDateStats], + loadedJobsDateStats: [...newLoadedJobsDateStats], + }); + }); + }; + calcAverageSuccessRate = (): number => { const { globalStats } = this.state; if (globalStats.length === 0) return 0; diff --git a/estela-web/src/pages/ProjectListPage/index.tsx b/estela-web/src/pages/ProjectListPage/index.tsx index b3ab7300..e43ddfb3 100644 --- a/estela-web/src/pages/ProjectListPage/index.tsx +++ b/estela-web/src/pages/ProjectListPage/index.tsx @@ -10,7 +10,7 @@ import FolderDotted from "../../assets/icons/folderDotted.svg"; import WelcomeProjects from "../../assets/images/welcomeProjects.svg"; import history from "../../history"; import { ApiProjectsListRequest, ApiProjectsCreateRequest, Project, ProjectCategoryEnum } from "../../services/api"; -import { incorrectDataNotification, Spin } from "../../shared"; +import { incorrectDataNotification, Spin, PaginationItem } from "../../shared"; import { UserContext, UserContextProps } from "../../context/UserContext"; const { Content } = Layout; @@ -92,7 +92,7 @@ export class ProjectListPage extends Component { emptyText = (): ReactElement => ( -

No proyects yet.

+

No projects yet.

); @@ -431,6 +431,7 @@ export class ProjectListPage extends Component { pageSize={this.PAGE_SIZE} onChange={this.onPageChange} showSizeChanger={false} + itemRender={PaginationItem} /> {this.totalProjects === 0 && ( diff --git a/estela-web/src/pages/ProjectMemberPage/index.tsx b/estela-web/src/pages/ProjectMemberPage/index.tsx index 98affdd9..f8951551 100644 --- a/estela-web/src/pages/ProjectMemberPage/index.tsx +++ b/estela-web/src/pages/ProjectMemberPage/index.tsx @@ -131,7 +131,6 @@ export class ProjectMemberPage extends Component> { + if (requestParameters.data === null || requestParameters.data === undefined) { + throw new runtime.RequiredError('data','Required parameter requestParameters.data was null or undefined when calling apiNotificationsCreate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: UserNotificationToJSON(requestParameters.data), + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserNotificationFromJSON(jsonValue)); + } + + /** + */ + async apiNotificationsCreate(requestParameters: ApiNotificationsCreateRequest): Promise { + const response = await this.apiNotificationsCreateRaw(requestParameters); + return await response.value(); + } + + /** + */ + async apiNotificationsDeleteRaw(requestParameters: ApiNotificationsDeleteRequest): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiNotificationsDelete.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }); + + return new runtime.VoidApiResponse(response); + } + + /** + */ + async apiNotificationsDelete(requestParameters: ApiNotificationsDeleteRequest): Promise { + await this.apiNotificationsDeleteRaw(requestParameters); + } + + /** + */ + async apiNotificationsListRaw(requestParameters: ApiNotificationsListRequest): Promise> { + const queryParameters: any = {}; + + if (requestParameters.page !== undefined) { + queryParameters['page'] = requestParameters.page; + } + + if (requestParameters.pageSize !== undefined) { + queryParameters['page_size'] = requestParameters.pageSize; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2001FromJSON(jsonValue)); + } + + /** + */ + async apiNotificationsList(requestParameters: ApiNotificationsListRequest): Promise { + const response = await this.apiNotificationsListRaw(requestParameters); + return await response.value(); + } + + /** + */ + async apiNotificationsPartialUpdateRaw(requestParameters: ApiNotificationsPartialUpdateRequest): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiNotificationsPartialUpdate.'); + } + + if (requestParameters.data === null || requestParameters.data === undefined) { + throw new runtime.RequiredError('data','Required parameter requestParameters.data was null or undefined when calling apiNotificationsPartialUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: UserNotificationToJSON(requestParameters.data), + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserNotificationFromJSON(jsonValue)); + } + + /** + */ + async apiNotificationsPartialUpdate(requestParameters: ApiNotificationsPartialUpdateRequest): Promise { + const response = await this.apiNotificationsPartialUpdateRaw(requestParameters); + return await response.value(); + } + + /** + */ + async apiNotificationsReadRaw(requestParameters: ApiNotificationsReadRequest): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiNotificationsRead.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserNotificationFromJSON(jsonValue)); + } + + /** + */ + async apiNotificationsRead(requestParameters: ApiNotificationsReadRequest): Promise { + const response = await this.apiNotificationsReadRaw(requestParameters); + return await response.value(); + } + + /** + */ + async apiNotificationsUpdateRaw(requestParameters: ApiNotificationsUpdateRequest): Promise> { + if (requestParameters.id === null || requestParameters.id === undefined) { + throw new runtime.RequiredError('id','Required parameter requestParameters.id was null or undefined when calling apiNotificationsUpdate.'); + } + + if (requestParameters.data === null || requestParameters.data === undefined) { + throw new runtime.RequiredError('data','Required parameter requestParameters.data was null or undefined when calling apiNotificationsUpdate.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); + } + const response = await this.request({ + path: `/api/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: UserNotificationUpdateToJSON(requestParameters.data), + }); + + return new runtime.JSONApiResponse(response, (jsonValue) => UserNotificationUpdateFromJSON(jsonValue)); + } + + /** + */ + async apiNotificationsUpdate(requestParameters: ApiNotificationsUpdateRequest): Promise { + const response = await this.apiNotificationsUpdateRaw(requestParameters); + return await response.value(); + } + /** */ async apiProjectsCreateRaw(requestParameters: ApiProjectsCreateRequest): Promise> { @@ -1073,7 +1315,7 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiProjectsDeploysListRaw(requestParameters: ApiProjectsDeploysListRequest): Promise> { + async apiProjectsDeploysListRaw(requestParameters: ApiProjectsDeploysListRequest): Promise> { if (requestParameters.pid === null || requestParameters.pid === undefined) { throw new runtime.RequiredError('pid','Required parameter requestParameters.pid was null or undefined when calling apiProjectsDeploysList.'); } @@ -1100,12 +1342,12 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2002FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2003FromJSON(jsonValue)); } /** */ - async apiProjectsDeploysList(requestParameters: ApiProjectsDeploysListRequest): Promise { + async apiProjectsDeploysList(requestParameters: ApiProjectsDeploysListRequest): Promise { const response = await this.apiProjectsDeploysListRaw(requestParameters); return await response.value(); } @@ -1270,7 +1512,7 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiProjectsListRaw(requestParameters: ApiProjectsListRequest): Promise> { + async apiProjectsListRaw(requestParameters: ApiProjectsListRequest): Promise> { const queryParameters: any = {}; if (requestParameters.page !== undefined) { @@ -1293,12 +1535,12 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2001FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2002FromJSON(jsonValue)); } /** */ - async apiProjectsList(requestParameters: ApiProjectsListRequest): Promise { + async apiProjectsList(requestParameters: ApiProjectsListRequest): Promise { const response = await this.apiProjectsListRaw(requestParameters); return await response.value(); } @@ -1454,7 +1696,7 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiProjectsSpidersCronjobsListRaw(requestParameters: ApiProjectsSpidersCronjobsListRequest): Promise> { + async apiProjectsSpidersCronjobsListRaw(requestParameters: ApiProjectsSpidersCronjobsListRequest): Promise> { if (requestParameters.pid === null || requestParameters.pid === undefined) { throw new runtime.RequiredError('pid','Required parameter requestParameters.pid was null or undefined when calling apiProjectsSpidersCronjobsList.'); } @@ -1489,12 +1731,12 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2004FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2005FromJSON(jsonValue)); } /** */ - async apiProjectsSpidersCronjobsList(requestParameters: ApiProjectsSpidersCronjobsListRequest): Promise { + async apiProjectsSpidersCronjobsList(requestParameters: ApiProjectsSpidersCronjobsListRequest): Promise { const response = await this.apiProjectsSpidersCronjobsListRaw(requestParameters); return await response.value(); } @@ -1764,7 +2006,7 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiProjectsSpidersJobsDataListRaw(requestParameters: ApiProjectsSpidersJobsDataListRequest): Promise> { + async apiProjectsSpidersJobsDataListRaw(requestParameters: ApiProjectsSpidersJobsDataListRequest): Promise> { if (requestParameters.jid === null || requestParameters.jid === undefined) { throw new runtime.RequiredError('jid','Required parameter requestParameters.jid was null or undefined when calling apiProjectsSpidersJobsDataList.'); } @@ -1803,19 +2045,19 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2006FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2007FromJSON(jsonValue)); } /** */ - async apiProjectsSpidersJobsDataList(requestParameters: ApiProjectsSpidersJobsDataListRequest): Promise { + async apiProjectsSpidersJobsDataList(requestParameters: ApiProjectsSpidersJobsDataListRequest): Promise { const response = await this.apiProjectsSpidersJobsDataListRaw(requestParameters); return await response.value(); } /** */ - async apiProjectsSpidersJobsListRaw(requestParameters: ApiProjectsSpidersJobsListRequest): Promise> { + async apiProjectsSpidersJobsListRaw(requestParameters: ApiProjectsSpidersJobsListRequest): Promise> { if (requestParameters.pid === null || requestParameters.pid === undefined) { throw new runtime.RequiredError('pid','Required parameter requestParameters.pid was null or undefined when calling apiProjectsSpidersJobsList.'); } @@ -1858,12 +2100,12 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2005FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2006FromJSON(jsonValue)); } /** */ - async apiProjectsSpidersJobsList(requestParameters: ApiProjectsSpidersJobsListRequest): Promise { + async apiProjectsSpidersJobsList(requestParameters: ApiProjectsSpidersJobsListRequest): Promise { const response = await this.apiProjectsSpidersJobsListRaw(requestParameters); return await response.value(); } @@ -2001,7 +2243,7 @@ export class ApiApi extends runtime.BaseAPI { /** */ - async apiProjectsSpidersListRaw(requestParameters: ApiProjectsSpidersListRequest): Promise> { + async apiProjectsSpidersListRaw(requestParameters: ApiProjectsSpidersListRequest): Promise> { if (requestParameters.pid === null || requestParameters.pid === undefined) { throw new runtime.RequiredError('pid','Required parameter requestParameters.pid was null or undefined when calling apiProjectsSpidersList.'); } @@ -2028,12 +2270,12 @@ export class ApiApi extends runtime.BaseAPI { query: queryParameters, }); - return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2003FromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => InlineResponse2004FromJSON(jsonValue)); } /** */ - async apiProjectsSpidersList(requestParameters: ApiProjectsSpidersListRequest): Promise { + async apiProjectsSpidersList(requestParameters: ApiProjectsSpidersListRequest): Promise { const response = await this.apiProjectsSpidersListRaw(requestParameters); return await response.value(); } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2001.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2001.ts index 2ff22852..348e0201 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2001.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2001.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - Project, - ProjectFromJSON, - ProjectFromJSONTyped, - ProjectToJSON, + UserNotification, + UserNotificationFromJSON, + UserNotificationFromJSONTyped, + UserNotificationToJSON, } from './'; /** @@ -46,10 +46,10 @@ export interface InlineResponse2001 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2001 */ - results: Array; + results: Array; } export function InlineResponse2001FromJSON(json: any): InlineResponse2001 { @@ -65,7 +65,7 @@ export function InlineResponse2001FromJSONTyped(json: any, ignoreDiscriminator: 'count': json['count'], 'next': !exists(json, 'next') ? undefined : json['next'], 'previous': !exists(json, 'previous') ? undefined : json['previous'], - 'results': ((json['results'] as Array).map(ProjectFromJSON)), + 'results': ((json['results'] as Array).map(UserNotificationFromJSON)), }; } @@ -81,7 +81,7 @@ export function InlineResponse2001ToJSON(value?: InlineResponse2001 | null): any 'count': value.count, 'next': value.next, 'previous': value.previous, - 'results': ((value.results as Array).map(ProjectToJSON)), + 'results': ((value.results as Array).map(UserNotificationToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2002.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2002.ts index 33395b4e..83eb08ad 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2002.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2002.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - Deploy, - DeployFromJSON, - DeployFromJSONTyped, - DeployToJSON, + Project, + ProjectFromJSON, + ProjectFromJSONTyped, + ProjectToJSON, } from './'; /** @@ -46,10 +46,10 @@ export interface InlineResponse2002 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2002 */ - results: Array; + results: Array; } export function InlineResponse2002FromJSON(json: any): InlineResponse2002 { @@ -65,7 +65,7 @@ export function InlineResponse2002FromJSONTyped(json: any, ignoreDiscriminator: 'count': json['count'], 'next': !exists(json, 'next') ? undefined : json['next'], 'previous': !exists(json, 'previous') ? undefined : json['previous'], - 'results': ((json['results'] as Array).map(DeployFromJSON)), + 'results': ((json['results'] as Array).map(ProjectFromJSON)), }; } @@ -81,7 +81,7 @@ export function InlineResponse2002ToJSON(value?: InlineResponse2002 | null): any 'count': value.count, 'next': value.next, 'previous': value.previous, - 'results': ((value.results as Array).map(DeployToJSON)), + 'results': ((value.results as Array).map(ProjectToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2003.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2003.ts index 89d74c30..d3ddc19c 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2003.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2003.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - Spider, - SpiderFromJSON, - SpiderFromJSONTyped, - SpiderToJSON, + Deploy, + DeployFromJSON, + DeployFromJSONTyped, + DeployToJSON, } from './'; /** @@ -46,10 +46,10 @@ export interface InlineResponse2003 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2003 */ - results: Array; + results: Array; } export function InlineResponse2003FromJSON(json: any): InlineResponse2003 { @@ -65,7 +65,7 @@ export function InlineResponse2003FromJSONTyped(json: any, ignoreDiscriminator: 'count': json['count'], 'next': !exists(json, 'next') ? undefined : json['next'], 'previous': !exists(json, 'previous') ? undefined : json['previous'], - 'results': ((json['results'] as Array).map(SpiderFromJSON)), + 'results': ((json['results'] as Array).map(DeployFromJSON)), }; } @@ -81,7 +81,7 @@ export function InlineResponse2003ToJSON(value?: InlineResponse2003 | null): any 'count': value.count, 'next': value.next, 'previous': value.previous, - 'results': ((value.results as Array).map(SpiderToJSON)), + 'results': ((value.results as Array).map(DeployToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2004.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2004.ts index 7fb9238c..ca9d6158 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2004.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2004.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - SpiderCronJob, - SpiderCronJobFromJSON, - SpiderCronJobFromJSONTyped, - SpiderCronJobToJSON, + Spider, + SpiderFromJSON, + SpiderFromJSONTyped, + SpiderToJSON, } from './'; /** @@ -46,10 +46,10 @@ export interface InlineResponse2004 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2004 */ - results: Array; + results: Array; } export function InlineResponse2004FromJSON(json: any): InlineResponse2004 { @@ -65,7 +65,7 @@ export function InlineResponse2004FromJSONTyped(json: any, ignoreDiscriminator: 'count': json['count'], 'next': !exists(json, 'next') ? undefined : json['next'], 'previous': !exists(json, 'previous') ? undefined : json['previous'], - 'results': ((json['results'] as Array).map(SpiderCronJobFromJSON)), + 'results': ((json['results'] as Array).map(SpiderFromJSON)), }; } @@ -81,7 +81,7 @@ export function InlineResponse2004ToJSON(value?: InlineResponse2004 | null): any 'count': value.count, 'next': value.next, 'previous': value.previous, - 'results': ((value.results as Array).map(SpiderCronJobToJSON)), + 'results': ((value.results as Array).map(SpiderToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2005.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2005.ts index dfc6b738..8f7a4a63 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2005.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2005.ts @@ -14,10 +14,10 @@ import { exists, mapValues } from '../runtime'; import { - SpiderJob, - SpiderJobFromJSON, - SpiderJobFromJSONTyped, - SpiderJobToJSON, + SpiderCronJob, + SpiderCronJobFromJSON, + SpiderCronJobFromJSONTyped, + SpiderCronJobToJSON, } from './'; /** @@ -46,10 +46,10 @@ export interface InlineResponse2005 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2005 */ - results: Array; + results: Array; } export function InlineResponse2005FromJSON(json: any): InlineResponse2005 { @@ -65,7 +65,7 @@ export function InlineResponse2005FromJSONTyped(json: any, ignoreDiscriminator: 'count': json['count'], 'next': !exists(json, 'next') ? undefined : json['next'], 'previous': !exists(json, 'previous') ? undefined : json['previous'], - 'results': ((json['results'] as Array).map(SpiderJobFromJSON)), + 'results': ((json['results'] as Array).map(SpiderCronJobFromJSON)), }; } @@ -81,7 +81,7 @@ export function InlineResponse2005ToJSON(value?: InlineResponse2005 | null): any 'count': value.count, 'next': value.next, 'previous': value.previous, - 'results': ((value.results as Array).map(SpiderJobToJSON)), + 'results': ((value.results as Array).map(SpiderCronJobToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2006.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2006.ts index 99688500..8f42084c 100644 --- a/estela-web/src/services/api/generated-api/models/InlineResponse2006.ts +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2006.ts @@ -13,6 +13,13 @@ */ import { exists, mapValues } from '../runtime'; +import { + SpiderJob, + SpiderJobFromJSON, + SpiderJobFromJSONTyped, + SpiderJobToJSON, +} from './'; + /** * * @export @@ -20,29 +27,29 @@ import { exists, mapValues } from '../runtime'; */ export interface InlineResponse2006 { /** - * Data items count. + * * @type {number} * @memberof InlineResponse2006 */ count: number; /** - * URI to the previous data chunk. + * * @type {string} * @memberof InlineResponse2006 */ - previous?: string | null; + next?: string | null; /** - * URI to the next data chunk. + * * @type {string} * @memberof InlineResponse2006 */ - next?: string | null; + previous?: string | null; /** - * Data items. - * @type {Array} + * + * @type {Array} * @memberof InlineResponse2006 */ - results?: Array; + results: Array; } export function InlineResponse2006FromJSON(json: any): InlineResponse2006 { @@ -56,9 +63,9 @@ export function InlineResponse2006FromJSONTyped(json: any, ignoreDiscriminator: return { 'count': json['count'], - 'previous': !exists(json, 'previous') ? undefined : json['previous'], 'next': !exists(json, 'next') ? undefined : json['next'], - 'results': !exists(json, 'results') ? undefined : json['results'], + 'previous': !exists(json, 'previous') ? undefined : json['previous'], + 'results': ((json['results'] as Array).map(SpiderJobFromJSON)), }; } @@ -72,9 +79,9 @@ export function InlineResponse2006ToJSON(value?: InlineResponse2006 | null): any return { 'count': value.count, - 'previous': value.previous, 'next': value.next, - 'results': value.results, + 'previous': value.previous, + 'results': ((value.results as Array).map(SpiderJobToJSON)), }; } diff --git a/estela-web/src/services/api/generated-api/models/InlineResponse2007.ts b/estela-web/src/services/api/generated-api/models/InlineResponse2007.ts new file mode 100644 index 00000000..095c0beb --- /dev/null +++ b/estela-web/src/services/api/generated-api/models/InlineResponse2007.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * estela API v1.0 Documentation + * estela API Swagger Specification + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface InlineResponse2007 + */ +export interface InlineResponse2007 { + /** + * Data items count. + * @type {number} + * @memberof InlineResponse2007 + */ + count: number; + /** + * URI to the previous data chunk. + * @type {string} + * @memberof InlineResponse2007 + */ + previous?: string | null; + /** + * URI to the next data chunk. + * @type {string} + * @memberof InlineResponse2007 + */ + next?: string | null; + /** + * Data items. + * @type {Array} + * @memberof InlineResponse2007 + */ + results?: Array; +} + +export function InlineResponse2007FromJSON(json: any): InlineResponse2007 { + return InlineResponse2007FromJSONTyped(json, false); +} + +export function InlineResponse2007FromJSONTyped(json: any, ignoreDiscriminator: boolean): InlineResponse2007 { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'count': json['count'], + 'previous': !exists(json, 'previous') ? undefined : json['previous'], + 'next': !exists(json, 'next') ? undefined : json['next'], + 'results': !exists(json, 'results') ? undefined : json['results'], + }; +} + +export function InlineResponse2007ToJSON(value?: InlineResponse2007 | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'count': value.count, + 'previous': value.previous, + 'next': value.next, + 'results': value.results, + }; +} + + diff --git a/estela-web/src/services/api/generated-api/models/Notification.ts b/estela-web/src/services/api/generated-api/models/Notification.ts new file mode 100644 index 00000000..34b75cc0 --- /dev/null +++ b/estela-web/src/services/api/generated-api/models/Notification.ts @@ -0,0 +1,91 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * estela API v1.0 Documentation + * estela API Swagger Specification + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import { + ProjectDetail, + ProjectDetailFromJSON, + ProjectDetailFromJSONTyped, + ProjectDetailToJSON, + UserDetail, + UserDetailFromJSON, + UserDetailFromJSONTyped, + UserDetailToJSON, +} from './'; + +/** + * Notification to which the user is subscribed. + * @export + * @interface Notification + */ +export interface Notification { + /** + * A unique integer value identifying each notification + * @type {number} + * @memberof Notification + */ + readonly nid?: number; + /** + * Notification message. + * @type {string} + * @memberof Notification + */ + message: string; + /** + * + * @type {UserDetail} + * @memberof Notification + */ + user: UserDetail; + /** + * + * @type {ProjectDetail} + * @memberof Notification + */ + project: ProjectDetail; +} + +export function NotificationFromJSON(json: any): Notification { + return NotificationFromJSONTyped(json, false); +} + +export function NotificationFromJSONTyped(json: any, ignoreDiscriminator: boolean): Notification { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'nid': !exists(json, 'nid') ? undefined : json['nid'], + 'message': json['message'], + 'user': UserDetailFromJSON(json['user']), + 'project': ProjectDetailFromJSON(json['project']), + }; +} + +export function NotificationToJSON(value?: Notification | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'message': value.message, + 'user': UserDetailToJSON(value.user), + 'project': ProjectDetailToJSON(value.project), + }; +} + + diff --git a/estela-web/src/services/api/generated-api/models/ProjectDetail.ts b/estela-web/src/services/api/generated-api/models/ProjectDetail.ts new file mode 100644 index 00000000..977d304e --- /dev/null +++ b/estela-web/src/services/api/generated-api/models/ProjectDetail.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * estela API v1.0 Documentation + * estela API Swagger Specification + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * Project where the action was performed. + * @export + * @interface ProjectDetail + */ +export interface ProjectDetail { + /** + * A UUID identifying this project. + * @type {string} + * @memberof ProjectDetail + */ + readonly pid?: string; + /** + * Project name. + * @type {string} + * @memberof ProjectDetail + */ + readonly name?: string; +} + +export function ProjectDetailFromJSON(json: any): ProjectDetail { + return ProjectDetailFromJSONTyped(json, false); +} + +export function ProjectDetailFromJSONTyped(json: any, ignoreDiscriminator: boolean): ProjectDetail { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'pid': !exists(json, 'pid') ? undefined : json['pid'], + 'name': !exists(json, 'name') ? undefined : json['name'], + }; +} + +export function ProjectDetailToJSON(value?: ProjectDetail | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + }; +} + + diff --git a/estela-web/src/services/api/generated-api/models/ProjectUpdate.ts b/estela-web/src/services/api/generated-api/models/ProjectUpdate.ts index 53255894..232fa186 100644 --- a/estela-web/src/services/api/generated-api/models/ProjectUpdate.ts +++ b/estela-web/src/services/api/generated-api/models/ProjectUpdate.ts @@ -33,11 +33,11 @@ export interface ProjectUpdate { */ readonly pid?: string; /** - * Project's name. + * Project name. * @type {string} * @memberof ProjectUpdate */ - name: string; + name?: string; /** * Affected users. * @type {Array} @@ -112,7 +112,7 @@ export function ProjectUpdateFromJSONTyped(json: any, ignoreDiscriminator: boole return { 'pid': !exists(json, 'pid') ? undefined : json['pid'], - 'name': json['name'], + 'name': !exists(json, 'name') ? undefined : json['name'], 'users': !exists(json, 'users') ? undefined : ((json['users'] as Array).map(UserDetailFromJSON)), 'email': !exists(json, 'email') ? undefined : json['email'], 'action': !exists(json, 'action') ? undefined : json['action'], diff --git a/estela-web/src/services/api/generated-api/models/SpiderJobUpdate.ts b/estela-web/src/services/api/generated-api/models/SpiderJobUpdate.ts index cbf6a009..d397c5f8 100644 --- a/estela-web/src/services/api/generated-api/models/SpiderJobUpdate.ts +++ b/estela-web/src/services/api/generated-api/models/SpiderJobUpdate.ts @@ -78,8 +78,6 @@ export enum SpiderJobUpdateStatusEnum { Waiting = 'WAITING', Running = 'RUNNING', Stopped = 'STOPPED', - Incomplete = 'INCOMPLETE', - Cancelled = 'CANCELLED', Completed = 'COMPLETED', Error = 'ERROR' }/** diff --git a/estela-web/src/services/api/generated-api/models/UserNotification.ts b/estela-web/src/services/api/generated-api/models/UserNotification.ts new file mode 100644 index 00000000..bfbc86cb --- /dev/null +++ b/estela-web/src/services/api/generated-api/models/UserNotification.ts @@ -0,0 +1,87 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * estela API v1.0 Documentation + * estela API Swagger Specification + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +import { + Notification, + NotificationFromJSON, + NotificationFromJSONTyped, + NotificationToJSON, +} from './'; + +/** + * + * @export + * @interface UserNotification + */ +export interface UserNotification { + /** + * Unique user notification ID. + * @type {number} + * @memberof UserNotification + */ + id: number; + /** + * + * @type {Notification} + * @memberof UserNotification + */ + notification: Notification; + /** + * Whether the notification was seen. + * @type {boolean} + * @memberof UserNotification + */ + seen?: boolean; + /** + * Date when the notification was sent. + * @type {Date} + * @memberof UserNotification + */ + readonly created?: Date; +} + +export function UserNotificationFromJSON(json: any): UserNotification { + return UserNotificationFromJSONTyped(json, false); +} + +export function UserNotificationFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserNotification { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'id': json['id'], + 'notification': NotificationFromJSON(json['notification']), + 'seen': !exists(json, 'seen') ? undefined : json['seen'], + 'created': !exists(json, 'created') ? undefined : (new Date(json['created'])), + }; +} + +export function UserNotificationToJSON(value?: UserNotification | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'id': value.id, + 'notification': NotificationToJSON(value.notification), + 'seen': value.seen, + }; +} + + diff --git a/estela-web/src/services/api/generated-api/models/UserNotificationUpdate.ts b/estela-web/src/services/api/generated-api/models/UserNotificationUpdate.ts new file mode 100644 index 00000000..d0c1219e --- /dev/null +++ b/estela-web/src/services/api/generated-api/models/UserNotificationUpdate.ts @@ -0,0 +1,64 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * estela API v1.0 Documentation + * estela API Swagger Specification + * + * The version of the OpenAPI document: v1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * + * @export + * @interface UserNotificationUpdate + */ +export interface UserNotificationUpdate { + /** + * + * @type {number} + * @memberof UserNotificationUpdate + */ + readonly id?: number; + /** + * Whether the user has seen the notification. + * @type {boolean} + * @memberof UserNotificationUpdate + */ + seen?: boolean; +} + +export function UserNotificationUpdateFromJSON(json: any): UserNotificationUpdate { + return UserNotificationUpdateFromJSONTyped(json, false); +} + +export function UserNotificationUpdateFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserNotificationUpdate { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'id': !exists(json, 'id') ? undefined : json['id'], + 'seen': !exists(json, 'seen') ? undefined : json['seen'], + }; +} + +export function UserNotificationUpdateToJSON(value?: UserNotificationUpdate | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'seen': value.seen, + }; +} + + diff --git a/estela-web/src/services/api/generated-api/models/index.ts b/estela-web/src/services/api/generated-api/models/index.ts index dea66dfe..725400e4 100644 --- a/estela-web/src/services/api/generated-api/models/index.ts +++ b/estela-web/src/services/api/generated-api/models/index.ts @@ -15,14 +15,17 @@ export * from './InlineResponse2003'; export * from './InlineResponse2004'; export * from './InlineResponse2005'; export * from './InlineResponse2006'; +export * from './InlineResponse2007'; export * from './InlineResponse401'; export * from './JobsMetadata'; export * from './JobsStats'; export * from './LogsStats'; +export * from './Notification'; export * from './PagesStats'; export * from './Permission'; export * from './Project'; export * from './ProjectCronJob'; +export * from './ProjectDetail'; export * from './ProjectJob'; export * from './ProjectUpdate'; export * from './ProjectUsage'; @@ -46,4 +49,6 @@ export * from './Token'; export * from './UsageRecord'; export * from './User'; export * from './UserDetail'; +export * from './UserNotification'; +export * from './UserNotificationUpdate'; export * from './UserProfile'; diff --git a/estela-web/src/services/auth.service.ts b/estela-web/src/services/auth.service.ts index 49c3861f..a07cf9d0 100644 --- a/estela-web/src/services/auth.service.ts +++ b/estela-web/src/services/auth.service.ts @@ -36,6 +36,7 @@ export const AuthService = { localStorage.removeItem(USERNAME_ROLE); }, setUserRole(role: string): void { + role = role.toLowerCase(); localStorage.setItem(USERNAME_ROLE, role); }, getUserEmail(): string | null { diff --git a/estela-web/src/shared/header/index.tsx b/estela-web/src/shared/header/index.tsx index 719aad2e..c2e82638 100644 --- a/estela-web/src/shared/header/index.tsx +++ b/estela-web/src/shared/header/index.tsx @@ -1,31 +1,41 @@ import React, { Component } from "react"; -import { Layout, Row, Col, Dropdown } from "antd"; +import { Layout, Row, Col, Dropdown, Badge } from "antd"; import type { MenuProps } from "antd"; import { Link } from "react-router-dom"; import history from "../../history"; -import { AuthService } from "../../services"; -import { NotificationsList } from "../../shared"; +import { AuthService, ApiService, ApiNotificationsListRequest, Notification } from "../../services"; import { UserContext, UserContextProps } from "../../context"; import User from "../../assets/icons/user.svg"; -import Notification from "../../assets/icons/notification.svg"; +import Message from "../../assets/icons/notification.svg"; import ArrowDown from "../../assets/icons/arrowDown.svg"; import Dashboard from "../../assets/icons/dashboard.svg"; import Settings from "../../assets/icons/setting.svg"; import Logout from "../../assets/icons/logout.svg"; +import Circle from "../../assets/icons/ellipse.svg"; import userDropdownSidenavItems from "ExternalComponents/DropdownComponent"; const { Header, Content } = Layout; type MenuItem = Required["items"][number]; -interface HeaderInterface { - path?: string; +interface HeaderState { + notifications: Notification[]; + loaded: boolean; + path: string; + news: boolean; } -export class CustomHeader extends Component { - constructor(props: HeaderInterface) { - super(props); +export class CustomHeader extends Component { + state: HeaderState = { + notifications: [], + loaded: false, + path: "", + news: false, + }; + timer: NodeJS.Timeout | undefined; + + async componentDidMount() { userDropdownSidenavItems.forEach((element: MenuItem) => { this.itemsUser?.push(element); }); @@ -42,10 +52,41 @@ export class CustomHeader extends Component { ), style: { backgroundColor: "white" }, }); + this.getNotifications(); + this.setState({ path: document.location.pathname }); + this.timer = setInterval(() => { + this.getNotifications(); + }, 15000); } - path = this.props.path; + + componentWillUnmount() { + clearInterval(this.timer); + } + + apiService = ApiService(); static contextType = UserContext; + getNotifications = async (): Promise => { + const requestParams: ApiNotificationsListRequest = { + pageSize: 3, + }; + this.apiService.apiNotificationsList(requestParams).then((response) => { + this.setState({ news: false }); + if (response.count === 0) { + this.setState({ loaded: true }); + return; + } + let change = false; + response.results.find((notification) => { + if (notification.seen === false) { + change = true; + } + }); + this.setState({ news: change }); + this.setState({ notifications: response.results, loaded: true }); + }); + }; + isLogged = (): boolean => { return Boolean(AuthService.getAuthToken()); }; @@ -73,6 +114,28 @@ export class CustomHeader extends Component { history.push("/login"); }; + renderNotificationIcon = (inbox: boolean, news: boolean): React.ReactNode => { + // const { news } = this.state; + const color = inbox + ? "stroke-estela-blue-full bg-estela-blue-low border border-estela-blue-full" + : "hover:stroke-estela-blue-full stroke-estela-black-full hover:bg-estela-blue-low"; + // const circleStyle = "fill-estela-red-full stroke-estela-red-full h-2"; + return ( + + + // : + // } + dot={news} + > + + + + ); + }; + itemsUser: MenuProps["items"] = [ { key: "1", @@ -114,12 +177,62 @@ export class CustomHeader extends Component { }, ]; - itemsNotification: MenuProps["items"] = [ + changeNotificationStatus(id: number): void { + const notifications = this.state.notifications; + const index = notifications.findIndex((user_notification) => user_notification.id == id); + if (notifications[index].seen) return; + + const requestData = { + seen: true, + }; + const requestParams: ApiNotificationsUpdateRequest = { + id: id, + data: requestData, + }; + + notifications[index].seen = true; + this.setState({ notifications: notifications }); + this.apiService.apiNotificationsUpdate(requestParams).then((response) => { + notifications[index].seen = response.seen; + this.setState({ notifications: notifications }); + }); + } + + notificationItems = (): MenuProps["items"] => [ { key: "1", label: ( - - + + {this.state.notifications.map((user_notification) => ( +
{ + this.changeNotificationStatus(user_notification.id); + event.stopPropagation(); + }} + className="py-2 px-3 flex cursor-pointer hover:bg-estela-blue-low hover:text-estela-blue-full rounded-md" + key={user_notification.notification?.nid} + > + {!user_notification.seen ? ( + }> + ) : ( +
+ )} +
+ + {user_notification.notification?.user.email == AuthService.getUserEmail() + ? "You" + : user_notification.notification?.user.username} + + {AuthService.getUserEmail() == user_notification.notification?.user.email + ? " have " + : " has "} + {user_notification.notification?.message} +

+ {user_notification.createdAt?.toDateString()} +

+
+
+ ))}
), style: { backgroundColor: "white" }, @@ -127,66 +240,91 @@ export class CustomHeader extends Component { { key: "2", label: ( - + { + this.setState({ path: "/notifications/inbox" }); + }} + > See all ), - disabled: true, + style: { backgroundColor: "white" }, + }, + ]; + + noNotifications = (): MenuProps["items"] => [ + { + key: "1", + label: ( + { + this.setState({ path: "/notifications/inbox" }); + }} + > + + You don't have any notifications yet. + + + ), style: { backgroundColor: "white" }, }, ]; render(): JSX.Element { + const { path, loaded, notifications, news } = this.state; return ( -
- - - - estela - - - - - {this.path === "/notifications/inbox" ? ( - - - - ) : ( - - - - )} - - - - - - - - - - - {this.getUser()} - {this.getUserRole() !== "" && ( - - {this.getUserRole()} - - )} - - - - - - - - - -
+ <> + {loaded ? ( +
+ + + + estela + + + + + {this.renderNotificationIcon(path === "/notifications/inbox", news)} + + + + + +
+ + + {this.getUser()} + {this.getUserRole() !== "" && ( + + {this.getUserRole()} + + )} + + +
+
+
+ +
+
+ ) : ( + <> + )} + ); } } diff --git a/estela-web/src/shared/index.tsx b/estela-web/src/shared/index.tsx index e9f6fbf4..44cc93f6 100644 --- a/estela-web/src/shared/index.tsx +++ b/estela-web/src/shared/index.tsx @@ -2,7 +2,6 @@ export { CustomHeader as Header } from "./header"; export { ProjectSidenav } from "./projectSidenav"; export { ProfileSettingsSideNav } from "./profileSettingsSidenav"; export { NotificationsSidenav } from "./notificationsSidenav"; -export { NotificationsList } from "./notificationsList"; export { CustomSpin as Spin } from "./spin"; export { PaginationItem } from "./paginationItem"; export * from "./notifications"; diff --git a/estela-web/src/shared/notificationsList/index.tsx b/estela-web/src/shared/notificationsList/index.tsx deleted file mode 100644 index 537376d4..00000000 --- a/estela-web/src/shared/notificationsList/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component } from "react"; -import { Layout, Space } from "antd"; - -import "./styles.scss"; - -import Ellipse from "../../assets/icons/ellipse.svg"; - -const { Content } = Layout; - -const notis = [ - { - id: 1, - seen: false, - message: "We are working on this feature.", - date: "Soon", - }, -]; - -export class NotificationsList extends Component { - render(): JSX.Element { - return ( - - {notis.map((notification) => { - return ( - - - - {!notification.seen ? ( - - ) : ( - - )} - - {notification.message} - {notification.date} - - - - - ); - })} - - ); - } -} diff --git a/estela-web/src/shared/notificationsList/styles.scss b/estela-web/src/shared/notificationsList/styles.scss deleted file mode 100644 index af0c0ae1..00000000 --- a/estela-web/src/shared/notificationsList/styles.scss +++ /dev/null @@ -1,59 +0,0 @@ -$estela: #4D47C3; - -.ant-menu-inline > .ant-menu-submenu:hover { - div { - color: $estela; - } - svg { - stroke: $estela; - } -} - -.ant-menu-inline > .ant-menu-item:hover { - a { - color: $estela; - } -} - -.ant-menu-inline > .ant-menu-submenu-selected { - .sub-menu { - color: $estela; - font-weight: 700; - background-color: #F6FAFD !important; - } - svg { - stroke: $estela; - } -} - -.ant-menu-inline > .ant-menu-item-selected { - div { - background-color: #F6FAFD; - } - a { - color: $estela; - font-weight: 700; - } - svg { - stroke: $estela; - } - background-color: white !important; -} - -.ant-menu-inline > .ant-menu-item::after, -.ant-menu-inline > .ant-menu-item::before { - border: none; -} - -.ant-menu-sub.ant-menu-inline { - background: white; -} - -.ant-layout-sider { - background-color: white; -} - -.ant-menu-inline { - box-shadow: none; - border-right: none; -} diff --git a/estela-web/src/shared/notificationsSidenav/index.tsx b/estela-web/src/shared/notificationsSidenav/index.tsx index efe02c93..c8339a98 100644 --- a/estela-web/src/shared/notificationsSidenav/index.tsx +++ b/estela-web/src/shared/notificationsSidenav/index.tsx @@ -22,7 +22,7 @@ export const NotificationsSidenav: React.FC = key: "inbox", label: ( - updatePath("inbox")} className="ml-2"> + updatePath("inbox")} className="mx-4"> Inbox @@ -35,7 +35,7 @@ export const NotificationsSidenav: React.FC = updatePath("settings")} - className="ml-2" + className="mx-4" > Settings @@ -48,8 +48,8 @@ export const NotificationsSidenav: React.FC = ]; return ( - - + + ); };