forked from github/plane
feat: github integration (#315)
* feat: initiate integrations * feat: initiate github integration create models for the same * feat: github integration views * fix: update workspace integration view to create bot users * refactor: rename repository model * refactor: update github repo sync endpoint to create repo and sync in one go * refactor: update issue activities to post the updates to segway hook * refactor: update endpoints to get project id and add actor as a member of project in repo sync * fix: make is bot as a read only field * fix: remove github repo imports * fix: url mapping * feat: repo views * refactor: update webhook request endpoint * refactor: rename repositories table to github_repositories * fix: workpace integration actor * feat: label for github integration * refactor: issue activity on create issue * refactor: repo create endpoint and add db constraints for repo sync and issues * feat: create api token on workpsace integration and avatar_url for integrations * refactor: add uuid primary key for Audit model * refactor: remove id from auditfield to maintain integrity and make avatar blank if none supplied * feat: track comments on an issue * feat: comment syncing from plane to github * fix: prevent activities created by bot to be sent to webhook * feat: github app installation id retrieve * feat: github app installation id saved into db * feat: installation_id for the github integragation and unique provider and project base integration for repo * refactor: remove actor logic from activity task * feat: saving github metadata using installation id in workspace integration table * feat: github repositories endpoint * feat: github and project repos synchronisation * feat: delete issue and delete comment activity * refactor: remove print logs * FIX: reading env names for github app while installation * refactor: update bot user firstname with title * fix: add is_bot value in field --------- Co-authored-by: venplane <venkatesh@plane.so>
This commit is contained in:
parent
c1a78cc230
commit
a9802f816e
@ -41,3 +41,12 @@ from .issue import (
|
|||||||
from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer
|
from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer
|
||||||
|
|
||||||
from .api_token import APITokenSerializer
|
from .api_token import APITokenSerializer
|
||||||
|
|
||||||
|
from .integration import (
|
||||||
|
IntegrationSerializer,
|
||||||
|
WorkspaceIntegrationSerializer,
|
||||||
|
GithubIssueSyncSerializer,
|
||||||
|
GithubRepositorySerializer,
|
||||||
|
GithubRepositorySyncSerializer,
|
||||||
|
GithubCommentSyncSerializer,
|
||||||
|
)
|
||||||
|
7
apiserver/plane/api/serializers/integration/__init__.py
Normal file
7
apiserver/plane/api/serializers/integration/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .base import IntegrationSerializer, WorkspaceIntegrationSerializer
|
||||||
|
from .github import (
|
||||||
|
GithubRepositorySerializer,
|
||||||
|
GithubRepositorySyncSerializer,
|
||||||
|
GithubIssueSyncSerializer,
|
||||||
|
GithubCommentSyncSerializer,
|
||||||
|
)
|
20
apiserver/plane/api/serializers/integration/base.py
Normal file
20
apiserver/plane/api/serializers/integration/base.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Module imports
|
||||||
|
from plane.api.serializers import BaseSerializer
|
||||||
|
from plane.db.models import Integration, WorkspaceIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Integration
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"verified",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceIntegrationSerializer(BaseSerializer):
|
||||||
|
integration_detail = IntegrationSerializer(read_only=True, source="integration")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WorkspaceIntegration
|
||||||
|
fields = "__all__"
|
45
apiserver/plane/api/serializers/integration/github.py
Normal file
45
apiserver/plane/api/serializers/integration/github.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Module imports
|
||||||
|
from plane.api.serializers import BaseSerializer
|
||||||
|
from plane.db.models import (
|
||||||
|
GithubIssueSync,
|
||||||
|
GithubRepository,
|
||||||
|
GithubRepositorySync,
|
||||||
|
GithubCommentSync,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepositorySerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = GithubRepository
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepositorySyncSerializer(BaseSerializer):
|
||||||
|
repo_detail = GithubRepositorySerializer(source="repository")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = GithubRepositorySync
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class GithubIssueSyncSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = GithubIssueSync
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"project",
|
||||||
|
"workspace",
|
||||||
|
"repository_sync",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GithubCommentSyncSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = GithubCommentSync
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"project",
|
||||||
|
"workspace",
|
||||||
|
"repository_sync",
|
||||||
|
"issue_sync",
|
||||||
|
]
|
@ -21,6 +21,7 @@ class UserSerializer(BaseSerializer):
|
|||||||
"last_login_uagent",
|
"last_login_uagent",
|
||||||
"token_updated_at",
|
"token_updated_at",
|
||||||
"is_onboarded",
|
"is_onboarded",
|
||||||
|
"is_bot",
|
||||||
]
|
]
|
||||||
extra_kwargs = {"password": {"write_only": True}}
|
extra_kwargs = {"password": {"write_only": True}}
|
||||||
|
|
||||||
@ -34,7 +35,9 @@ class UserLiteSerializer(BaseSerializer):
|
|||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
"avatar",
|
"avatar",
|
||||||
|
"is_bot",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
|
"is_bot",
|
||||||
]
|
]
|
||||||
|
@ -86,6 +86,14 @@ from plane.api.views import (
|
|||||||
# Api Tokens
|
# Api Tokens
|
||||||
ApiTokenEndpoint,
|
ApiTokenEndpoint,
|
||||||
## End Api Tokens
|
## End Api Tokens
|
||||||
|
# Integrations
|
||||||
|
IntegrationViewSet,
|
||||||
|
WorkspaceIntegrationViewSet,
|
||||||
|
GithubRepositoriesEndpoint,
|
||||||
|
GithubRepositorySyncViewSet,
|
||||||
|
GithubIssueSyncViewSet,
|
||||||
|
GithubCommentSyncViewSet,
|
||||||
|
## End Integrations
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -681,7 +689,118 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
## End Modules
|
## End Modules
|
||||||
# API Tokens
|
# API Tokens
|
||||||
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-token"),
|
path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-token"),
|
path("api-tokens/<uuid:pk>/", ApiTokenEndpoint.as_view(), name="api-tokens"),
|
||||||
## End API Tokens
|
## End API Tokens
|
||||||
|
# Integrations
|
||||||
|
path(
|
||||||
|
"integrations/",
|
||||||
|
IntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"integrations/<uuid:pk>/",
|
||||||
|
IntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<str:provider>/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<uuid:pk>/",
|
||||||
|
WorkspaceIntegrationViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="workspace-integrations",
|
||||||
|
),
|
||||||
|
# Github Integrations
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/workspace-integrations/<uuid:workspace_integration_id>/github-repositories/",
|
||||||
|
GithubRepositoriesEndpoint.as_view(),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/",
|
||||||
|
GithubRepositorySyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/workspace-integrations/<uuid:workspace_integration_id>/github-repository-sync/<uuid:pk>/",
|
||||||
|
GithubRepositorySyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/",
|
||||||
|
GithubIssueSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:pk>/",
|
||||||
|
GithubIssueSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/",
|
||||||
|
GithubCommentSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "create",
|
||||||
|
"get": "list",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/github-repository-sync/<uuid:repo_sync_id>/github-issue-sync/<uuid:issue_sync_id>/github-comment-sync/<uuid:pk>/",
|
||||||
|
GithubCommentSyncViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
## End Github Integrations
|
||||||
|
## End Integrations
|
||||||
]
|
]
|
||||||
|
@ -73,3 +73,12 @@ from .authentication import (
|
|||||||
from .module import ModuleViewSet, ModuleIssueViewSet
|
from .module import ModuleViewSet, ModuleIssueViewSet
|
||||||
|
|
||||||
from .api_token import ApiTokenEndpoint
|
from .api_token import ApiTokenEndpoint
|
||||||
|
|
||||||
|
from .integration import (
|
||||||
|
WorkspaceIntegrationViewSet,
|
||||||
|
IntegrationViewSet,
|
||||||
|
GithubIssueSyncViewSet,
|
||||||
|
GithubRepositorySyncViewSet,
|
||||||
|
GithubCommentSyncViewSet,
|
||||||
|
GithubRepositoriesEndpoint,
|
||||||
|
)
|
||||||
|
7
apiserver/plane/api/views/integration/__init__.py
Normal file
7
apiserver/plane/api/views/integration/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .base import IntegrationViewSet, WorkspaceIntegrationViewSet
|
||||||
|
from .github import (
|
||||||
|
GithubRepositorySyncViewSet,
|
||||||
|
GithubIssueSyncViewSet,
|
||||||
|
GithubCommentSyncViewSet,
|
||||||
|
GithubRepositoriesEndpoint,
|
||||||
|
)
|
159
apiserver/plane/api/views/integration/base.py
Normal file
159
apiserver/plane/api/views/integration/base.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Python improts
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.api.views import BaseViewSet
|
||||||
|
from plane.db.models import (
|
||||||
|
Integration,
|
||||||
|
WorkspaceIntegration,
|
||||||
|
Workspace,
|
||||||
|
User,
|
||||||
|
WorkspaceMember,
|
||||||
|
APIToken,
|
||||||
|
)
|
||||||
|
from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
|
||||||
|
from plane.utils.integrations.github import get_github_metadata
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationViewSet(BaseViewSet):
|
||||||
|
serializer_class = IntegrationSerializer
|
||||||
|
model = Integration
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
try:
|
||||||
|
serializer = IntegrationSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, pk):
|
||||||
|
try:
|
||||||
|
integration = Integration.objects.get(pk=pk)
|
||||||
|
if integration.verified:
|
||||||
|
return Response(
|
||||||
|
{"error": "Verified integrations cannot be updated"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IntegrationSerializer(
|
||||||
|
integration, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Integration.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Integration Does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceIntegrationViewSet(BaseViewSet):
|
||||||
|
serializer_class = WorkspaceIntegrationSerializer
|
||||||
|
model = WorkspaceIntegration
|
||||||
|
|
||||||
|
def create(self, request, slug, provider):
|
||||||
|
try:
|
||||||
|
installation_id = request.data.get("installation_id", None)
|
||||||
|
|
||||||
|
if not installation_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Installation ID is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
integration = Integration.objects.get(provider=provider)
|
||||||
|
config = {}
|
||||||
|
if provider == "github":
|
||||||
|
metadata = get_github_metadata(installation_id)
|
||||||
|
config = {"installation_id": installation_id}
|
||||||
|
|
||||||
|
# Create a bot user
|
||||||
|
bot_user = User.objects.create(
|
||||||
|
email=f"{uuid.uuid4().hex}@plane.so",
|
||||||
|
username=uuid.uuid4().hex,
|
||||||
|
password=make_password(uuid.uuid4().hex),
|
||||||
|
is_password_autoset=True,
|
||||||
|
is_bot=True,
|
||||||
|
first_name=integration.title,
|
||||||
|
avatar=integration.avatar_url
|
||||||
|
if integration.avatar_url is not None
|
||||||
|
else "",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an API Token for the bot user
|
||||||
|
api_token = APIToken.objects.create(
|
||||||
|
user=bot_user,
|
||||||
|
user_type=1, # bot user
|
||||||
|
workspace=workspace,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.create(
|
||||||
|
workspace=workspace,
|
||||||
|
integration=integration,
|
||||||
|
actor=bot_user,
|
||||||
|
api_token=api_token,
|
||||||
|
metadata=metadata,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add bot user as a member of workspace
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace=workspace_integration.workspace,
|
||||||
|
member=bot_user,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
WorkspaceIntegrationSerializer(workspace_integration).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"error": "Integration is already active in the workspace"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except (Workspace.DoesNotExist, Integration.DoesNotExist) as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace or Integration not found"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
145
apiserver/plane/api/views/integration/github.py
Normal file
145
apiserver/plane/api/views/integration/github.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.api.views import BaseViewSet, BaseAPIView
|
||||||
|
from plane.db.models import (
|
||||||
|
GithubIssueSync,
|
||||||
|
GithubRepositorySync,
|
||||||
|
GithubRepository,
|
||||||
|
WorkspaceIntegration,
|
||||||
|
ProjectMember,
|
||||||
|
Label,
|
||||||
|
GithubCommentSync,
|
||||||
|
)
|
||||||
|
from plane.api.serializers import (
|
||||||
|
GithubIssueSyncSerializer,
|
||||||
|
GithubRepositorySyncSerializer,
|
||||||
|
GithubCommentSyncSerializer,
|
||||||
|
)
|
||||||
|
from plane.utils.integrations.github import get_github_repos
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepositoriesEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug, workspace_integration_id):
|
||||||
|
try:
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
|
workspace__slug=slug, pk=workspace_integration_id
|
||||||
|
)
|
||||||
|
access_tokens_url = workspace_integration.metadata["access_tokens_url"]
|
||||||
|
repositories_url = workspace_integration.metadata["repositories_url"]
|
||||||
|
repositories = get_github_repos(access_tokens_url, repositories_url)
|
||||||
|
return Response(repositories, status=status.HTTP_200_OK)
|
||||||
|
except WorkspaceIntegration.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace Integration Does not exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepositorySyncViewSet(BaseViewSet):
|
||||||
|
serializer_class = GithubRepositorySyncSerializer
|
||||||
|
model = GithubRepositorySync
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, workspace_integration_id):
|
||||||
|
try:
|
||||||
|
name = request.data.get("name", False)
|
||||||
|
url = request.data.get("url", False)
|
||||||
|
config = request.data.get("config", {})
|
||||||
|
repository_id = request.data.get("repository_id", False)
|
||||||
|
owner = request.data.get("owner", False)
|
||||||
|
|
||||||
|
if not name or not url or not repository_id or not owner:
|
||||||
|
return Response(
|
||||||
|
{"error": "Name, url, repository_id and owner are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create repository
|
||||||
|
repo = GithubRepository.objects.create(
|
||||||
|
name=name,
|
||||||
|
url=url,
|
||||||
|
config=config,
|
||||||
|
repository_id=repository_id,
|
||||||
|
owner=owner,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the workspace integration
|
||||||
|
workspace_integration = WorkspaceIntegration.objects.get(
|
||||||
|
pk=workspace_integration_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a Label for github
|
||||||
|
label = Label.objects.filter(
|
||||||
|
name="GitHub",
|
||||||
|
project_id=project_id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if label is None:
|
||||||
|
label = Label.objects.create(
|
||||||
|
name="GitHub",
|
||||||
|
project_id=project_id,
|
||||||
|
description="Label to sync Plane issues with GitHub issues",
|
||||||
|
color="#003773",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create repo sync
|
||||||
|
repo_sync = GithubRepositorySync.objects.create(
|
||||||
|
repository=repo,
|
||||||
|
workspace_integration=workspace_integration,
|
||||||
|
actor=workspace_integration.actor,
|
||||||
|
credentials=request.data.get("credentials", {}),
|
||||||
|
project_id=project_id,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add bot as a member in the project
|
||||||
|
_ = ProjectMember.objects.create(
|
||||||
|
member=workspace_integration.actor, role=20, project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return Response
|
||||||
|
return Response(
|
||||||
|
GithubRepositorySyncSerializer(repo_sync).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
except WorkspaceIntegration.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace Integration does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubIssueSyncViewSet(BaseViewSet):
|
||||||
|
serializer_class = GithubIssueSyncSerializer
|
||||||
|
model = GithubIssueSync
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
repository_sync_id=self.kwargs.get("repo_sync_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubCommentSyncViewSet(BaseViewSet):
|
||||||
|
serializer_class = GithubCommentSyncSerializer
|
||||||
|
model = GithubCommentSync
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
issue_sync_id=self.kwargs.get("issue_sync_id"),
|
||||||
|
)
|
@ -3,7 +3,7 @@ import json
|
|||||||
from itertools import groupby, chain
|
from itertools import groupby, chain
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db.models import Prefetch, OuterRef, Func, F
|
from django.db.models import Prefetch, OuterRef, Func, F, Q
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
@ -80,7 +80,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
if current_instance is not None:
|
if current_instance is not None:
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
{
|
{
|
||||||
"type": "issue.activity",
|
"type": "issue.activity.updated",
|
||||||
"requested_data": requested_data,
|
"requested_data": requested_data,
|
||||||
"actor_id": str(self.request.user.id),
|
"actor_id": str(self.request.user.id),
|
||||||
"issue_id": str(self.kwargs.get("pk", None)),
|
"issue_id": str(self.kwargs.get("pk", None)),
|
||||||
@ -93,6 +93,27 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
{
|
||||||
|
"type": "issue.activity.deleted",
|
||||||
|
"requested_data": json.dumps(
|
||||||
|
{"issue_id": str(self.kwargs.get("pk", None))}
|
||||||
|
),
|
||||||
|
"actor_id": str(self.request.user.id),
|
||||||
|
"issue_id": str(self.kwargs.get("pk", None)),
|
||||||
|
"project_id": str(self.kwargs.get("project_id", None)),
|
||||||
|
"current_instance": json.dumps(
|
||||||
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
@ -193,15 +214,18 @@ class IssueViewSet(BaseViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
# Track the issue
|
# Track the issue
|
||||||
IssueActivity.objects.create(
|
issue_activity.delay(
|
||||||
issue_id=serializer.data["id"],
|
{
|
||||||
project_id=project_id,
|
"type": "issue.activity.created",
|
||||||
workspace_id=serializer["workspace"],
|
"requested_data": json.dumps(
|
||||||
comment=f"{request.user.email} created the issue",
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
verb="created",
|
),
|
||||||
actor=request.user,
|
"actor_id": str(request.user.id),
|
||||||
|
"issue_id": str(serializer.data.get("id", None)),
|
||||||
|
"project_id": str(project_id),
|
||||||
|
"current_instance": None,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -304,7 +328,10 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
issue_activities = (
|
issue_activities = (
|
||||||
IssueActivity.objects.filter(issue_id=issue_id)
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
.filter(project__project_projectmember__member=self.request.user)
|
.filter(
|
||||||
|
~Q(field="comment"),
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
)
|
||||||
.select_related("actor")
|
.select_related("actor")
|
||||||
).order_by("created_by")
|
).order_by("created_by")
|
||||||
issue_comments = (
|
issue_comments = (
|
||||||
@ -347,6 +374,60 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
issue_id=self.kwargs.get("issue_id"),
|
issue_id=self.kwargs.get("issue_id"),
|
||||||
actor=self.request.user if self.request.user is not None else None,
|
actor=self.request.user if self.request.user is not None else None,
|
||||||
)
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
{
|
||||||
|
"type": "comment.activity.created",
|
||||||
|
"requested_data": json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||||
|
"actor_id": str(self.request.user.id),
|
||||||
|
"issue_id": str(self.kwargs.get("issue_id")),
|
||||||
|
"project_id": str(self.kwargs.get("project_id")),
|
||||||
|
"current_instance": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
{
|
||||||
|
"type": "comment.activity.updated",
|
||||||
|
"requested_data": requested_data,
|
||||||
|
"actor_id": str(self.request.user.id),
|
||||||
|
"issue_id": str(self.kwargs.get("issue_id", None)),
|
||||||
|
"project_id": str(self.kwargs.get("project_id", None)),
|
||||||
|
"current_instance": json.dumps(
|
||||||
|
IssueCommentSerializer(current_instance).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
current_instance = (
|
||||||
|
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
||||||
|
)
|
||||||
|
if current_instance is not None:
|
||||||
|
issue_activity.delay(
|
||||||
|
{
|
||||||
|
"type": "comment.activity.deleted",
|
||||||
|
"requested_data": json.dumps(
|
||||||
|
{"comment_id": str(self.kwargs.get("pk", None))}
|
||||||
|
),
|
||||||
|
"actor_id": str(self.request.user.id),
|
||||||
|
"issue_id": str(self.kwargs.get("issue_id", None)),
|
||||||
|
"project_id": str(self.kwargs.get("project_id", None)),
|
||||||
|
"current_instance": json.dumps(
|
||||||
|
IssueCommentSerializer(current_instance).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from django_rq import job
|
from django_rq import job
|
||||||
@ -16,6 +21,7 @@ from plane.db.models import (
|
|||||||
Cycle,
|
Cycle,
|
||||||
Module,
|
Module,
|
||||||
)
|
)
|
||||||
|
from plane.api.serializers import IssueActivitySerializer
|
||||||
|
|
||||||
|
|
||||||
# Track Chnages in name
|
# Track Chnages in name
|
||||||
@ -612,22 +618,24 @@ def track_modules(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Receive message from room group
|
def create_issue_activity(
|
||||||
@job("default")
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
def issue_activity(event):
|
):
|
||||||
try:
|
issue_activities.append(
|
||||||
issue_activities = []
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} created the issue",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
requested_data = json.loads(event.get("requested_data"))
|
|
||||||
current_instance = json.loads(event.get("current_instance"))
|
|
||||||
issue_id = event.get("issue_id", None)
|
|
||||||
actor_id = event.get("actor_id")
|
|
||||||
project_id = event.get("project_id")
|
|
||||||
|
|
||||||
actor = User.objects.get(pk=actor_id)
|
|
||||||
|
|
||||||
project = Project.objects.get(pk=project_id)
|
|
||||||
|
|
||||||
|
def update_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
ISSUE_ACTIVITY_MAPPER = {
|
ISSUE_ACTIVITY_MAPPER = {
|
||||||
"name": track_name,
|
"name": track_name,
|
||||||
"parent": track_parent,
|
"parent": track_parent,
|
||||||
@ -643,7 +651,6 @@ def issue_activity(event):
|
|||||||
"cycles_list": track_cycles,
|
"cycles_list": track_cycles,
|
||||||
"modules_list": track_modules,
|
"modules_list": track_modules,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in requested_data:
|
for key in requested_data:
|
||||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
@ -656,9 +663,136 @@ def issue_activity(event):
|
|||||||
issue_activities,
|
issue_activities,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save all the values to database
|
|
||||||
_ = IssueActivity.objects.bulk_create(issue_activities)
|
|
||||||
|
|
||||||
|
def create_comment_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} created a comment",
|
||||||
|
verb="created",
|
||||||
|
actor=actor,
|
||||||
|
field="comment",
|
||||||
|
new_value=requested_data.get("comment_html"),
|
||||||
|
new_identifier=requested_data.get("id"),
|
||||||
|
issue_comment_id=requested_data.get("id", None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_comment_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
if current_instance.get("comment_html") != requested_data.get("comment_html"):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} updated a comment",
|
||||||
|
verb="updated",
|
||||||
|
actor=actor,
|
||||||
|
field="comment",
|
||||||
|
old_value=current_instance.get("comment_html"),
|
||||||
|
old_identifier=current_instance.get("id"),
|
||||||
|
new_value=requested_data.get("comment_html"),
|
||||||
|
new_identifier=current_instance.get("id"),
|
||||||
|
issue_comment_id=current_instance.get("id"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_issue_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} deleted the issue",
|
||||||
|
verb="deleted",
|
||||||
|
actor=actor,
|
||||||
|
field="issue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_comment_activity(
|
||||||
|
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||||
|
):
|
||||||
|
issue_activities.append(
|
||||||
|
IssueActivity(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project=project,
|
||||||
|
workspace=project.workspace,
|
||||||
|
comment=f"{actor.email} deleted the comment",
|
||||||
|
verb="deleted",
|
||||||
|
actor=actor,
|
||||||
|
field="comment",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Receive message from room group
|
||||||
|
@job("default")
|
||||||
|
def issue_activity(event):
|
||||||
|
try:
|
||||||
|
issue_activities = []
|
||||||
|
type = event.get("type")
|
||||||
|
requested_data = json.loads(event.get("requested_data"))
|
||||||
|
current_instance = (
|
||||||
|
json.loads(event.get("current_instance"))
|
||||||
|
if event.get("current_instance") is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
issue_id = event.get("issue_id", None)
|
||||||
|
actor_id = event.get("actor_id")
|
||||||
|
project_id = event.get("project_id")
|
||||||
|
|
||||||
|
actor = User.objects.get(pk=actor_id)
|
||||||
|
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
ACTIVITY_MAPPER = {
|
||||||
|
"issue.activity.created": create_issue_activity,
|
||||||
|
"issue.activity.updated": update_issue_activity,
|
||||||
|
"issue.activity.deleted": delete_issue_activity,
|
||||||
|
"comment.activity.created": create_comment_activity,
|
||||||
|
"comment.activity.updated": update_comment_activity,
|
||||||
|
"comment.activity.deleted": delete_comment_activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
func = ACTIVITY_MAPPER.get(type)
|
||||||
|
if func is not None:
|
||||||
|
func(
|
||||||
|
requested_data,
|
||||||
|
current_instance,
|
||||||
|
issue_id,
|
||||||
|
project,
|
||||||
|
actor,
|
||||||
|
issue_activities,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save all the values to database
|
||||||
|
issue_activities_created = IssueActivity.objects.bulk_create(issue_activities)
|
||||||
|
# Post the updates to segway for integrations and webhooks
|
||||||
|
if len(issue_activities_created):
|
||||||
|
# Don't send activities if the actor is a bot
|
||||||
|
if settings.PROXY_BASE_URL:
|
||||||
|
for issue_activity in issue_activities_created:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
issue_activity_json = json.dumps(
|
||||||
|
IssueActivitySerializer(issue_activity).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
_ = requests.post(
|
||||||
|
f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/",
|
||||||
|
json=issue_activity_json,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
capture_exception(e)
|
capture_exception(e)
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django imports
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,13 @@ from .workspace import (
|
|||||||
TeamMember,
|
TeamMember,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .project import Project, ProjectMember, ProjectBaseModel, ProjectMemberInvite, ProjectIdentifier
|
from .project import (
|
||||||
|
Project,
|
||||||
|
ProjectMember,
|
||||||
|
ProjectBaseModel,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectIdentifier,
|
||||||
|
)
|
||||||
|
|
||||||
from .issue import (
|
from .issue import (
|
||||||
Issue,
|
Issue,
|
||||||
@ -41,3 +47,12 @@ from .view import View
|
|||||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink
|
from .module import Module, ModuleMember, ModuleIssue, ModuleLink
|
||||||
|
|
||||||
from .api_token import APIToken
|
from .api_token import APIToken
|
||||||
|
|
||||||
|
from .integration import (
|
||||||
|
WorkspaceIntegration,
|
||||||
|
Integration,
|
||||||
|
GithubRepository,
|
||||||
|
GithubRepositorySync,
|
||||||
|
GithubIssueSync,
|
||||||
|
GithubCommentSync,
|
||||||
|
)
|
||||||
|
2
apiserver/plane/db/models/integration/__init__.py
Normal file
2
apiserver/plane/db/models/integration/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .base import Integration, WorkspaceIntegration
|
||||||
|
from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync
|
68
apiserver/plane/db/models/integration/base.py
Normal file
68
apiserver/plane/db/models/integration/base.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import BaseModel
|
||||||
|
from plane.db.mixins import AuditModel
|
||||||
|
|
||||||
|
|
||||||
|
class Integration(AuditModel):
|
||||||
|
id = models.UUIDField(
|
||||||
|
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
|
||||||
|
)
|
||||||
|
title = models.CharField(max_length=400)
|
||||||
|
provider = models.CharField(max_length=400, unique=True)
|
||||||
|
network = models.PositiveIntegerField(
|
||||||
|
default=1, choices=((1, "Private"), (2, "Public"))
|
||||||
|
)
|
||||||
|
description = models.JSONField(default=dict)
|
||||||
|
author = models.CharField(max_length=400, blank=True)
|
||||||
|
webhook_url = models.TextField(blank=True)
|
||||||
|
webhook_secret = models.TextField(blank=True)
|
||||||
|
redirect_url = models.TextField(blank=True)
|
||||||
|
metadata = models.JSONField(default=dict)
|
||||||
|
verified = models.BooleanField(default=False)
|
||||||
|
avatar_url = models.URLField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return provider of the integration"""
|
||||||
|
return f"{self.provider}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Integration"
|
||||||
|
verbose_name_plural = "Integrations"
|
||||||
|
db_table = "integrations"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceIntegration(BaseModel):
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
# Bot user
|
||||||
|
actor = models.ForeignKey(
|
||||||
|
"db.User", related_name="integrations", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
integration = models.ForeignKey(
|
||||||
|
"db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
api_token = models.ForeignKey(
|
||||||
|
"db.APIToken", related_name="integrations", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
metadata = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
config = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return name of the integration and workspace"""
|
||||||
|
return f"{self.workspace.name} <{self.integration.provider}>"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["workspace", "integration"]
|
||||||
|
verbose_name = "Workspace Integration"
|
||||||
|
verbose_name_plural = "Workspace Integrations"
|
||||||
|
db_table = "workspace_integrations"
|
||||||
|
ordering = ("-created_at",)
|
99
apiserver/plane/db/models/integration/github.py
Normal file
99
apiserver/plane/db/models/integration/github.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.db.models import ProjectBaseModel
|
||||||
|
from plane.db.mixins import AuditModel
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepository(ProjectBaseModel):
|
||||||
|
name = models.CharField(max_length=500)
|
||||||
|
url = models.URLField(null=True)
|
||||||
|
config = models.JSONField(default=dict)
|
||||||
|
repository_id = models.BigIntegerField()
|
||||||
|
owner = models.CharField(max_length=500)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the repo name"""
|
||||||
|
return f"{self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Repository"
|
||||||
|
verbose_name_plural = "Repositories"
|
||||||
|
db_table = "github_repositories"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubRepositorySync(ProjectBaseModel):
|
||||||
|
repository = models.OneToOneField(
|
||||||
|
"db.GithubRepository", on_delete=models.CASCADE, related_name="syncs"
|
||||||
|
)
|
||||||
|
credentials = models.JSONField(default=dict)
|
||||||
|
# Bot user
|
||||||
|
actor = models.ForeignKey(
|
||||||
|
"db.User", related_name="user_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
workspace_integration = models.ForeignKey(
|
||||||
|
"db.WorkspaceIntegration", related_name="github_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
label = models.ForeignKey(
|
||||||
|
"db.Label", on_delete=models.SET_NULL, null=True, related_name="repo_syncs"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the repo sync"""
|
||||||
|
return f"{self.repository.name} <{self.project.name}>"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["project", "repository"]
|
||||||
|
verbose_name = "Github Repository Sync"
|
||||||
|
verbose_name_plural = "Github Repository Syncs"
|
||||||
|
db_table = "github_repository_syncs"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubIssueSync(ProjectBaseModel):
|
||||||
|
repo_issue_id = models.BigIntegerField()
|
||||||
|
github_issue_id = models.BigIntegerField()
|
||||||
|
issue_url = models.URLField(blank=False)
|
||||||
|
issue = models.ForeignKey(
|
||||||
|
"db.Issue", related_name="github_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
repository_sync = models.ForeignKey(
|
||||||
|
"db.GithubRepositorySync", related_name="issue_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the github issue sync"""
|
||||||
|
return f"{self.repository.name}-{self.project.name}-{self.issue.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["repository_sync", "issue"]
|
||||||
|
verbose_name = "Github Issue Sync"
|
||||||
|
verbose_name_plural = "Github Issue Syncs"
|
||||||
|
db_table = "github_issue_syncs"
|
||||||
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
|
||||||
|
class GithubCommentSync(ProjectBaseModel):
|
||||||
|
repo_comment_id = models.BigIntegerField()
|
||||||
|
comment = models.ForeignKey(
|
||||||
|
"db.IssueComment", related_name="comment_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
issue_sync = models.ForeignKey(
|
||||||
|
"db.GithubIssueSync", related_name="comment_syncs", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return the github issue sync"""
|
||||||
|
return f"{self.comment.id}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ["issue_sync", "comment"]
|
||||||
|
verbose_name = "Github Comment Sync"
|
||||||
|
verbose_name_plural = "Github Comment Syncs"
|
||||||
|
db_table = "github_comment_syncs"
|
||||||
|
ordering = ("-created_at",)
|
@ -187,7 +187,7 @@ class IssueLink(ProjectBaseModel):
|
|||||||
|
|
||||||
class IssueActivity(ProjectBaseModel):
|
class IssueActivity(ProjectBaseModel):
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
Issue, on_delete=models.CASCADE, related_name="issue_activity"
|
Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity"
|
||||||
)
|
)
|
||||||
verb = models.CharField(max_length=255, verbose_name="Action", default="created")
|
verb = models.CharField(max_length=255, verbose_name="Action", default="created")
|
||||||
field = models.CharField(
|
field = models.CharField(
|
||||||
|
@ -77,3 +77,4 @@ if DOCKERIZED:
|
|||||||
REDIS_URL = os.environ.get("REDIS_URL")
|
REDIS_URL = os.environ.get("REDIS_URL")
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL", "localhost:3000")
|
WEB_URL = os.environ.get("WEB_URL", "localhost:3000")
|
||||||
|
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
||||||
|
@ -209,3 +209,5 @@ RQ_QUEUES = {
|
|||||||
|
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL")
|
WEB_URL = os.environ.get("WEB_URL")
|
||||||
|
|
||||||
|
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
||||||
|
@ -185,3 +185,5 @@ RQ_QUEUES = {
|
|||||||
|
|
||||||
|
|
||||||
WEB_URL = os.environ.get("WEB_URL")
|
WEB_URL = os.environ.get("WEB_URL")
|
||||||
|
|
||||||
|
PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False)
|
||||||
|
0
apiserver/plane/utils/integrations/__init__.py
Normal file
0
apiserver/plane/utils/integrations/__init__.py
Normal file
62
apiserver/plane/utils/integrations/github.py
Normal file
62
apiserver/plane/utils/integrations/github.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
|
||||||
|
def get_jwt_token():
|
||||||
|
app_id = os.environ.get("GITHUB_APP_ID", "")
|
||||||
|
secret = bytes(os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8")
|
||||||
|
current_timestamp = int(datetime.now().timestamp())
|
||||||
|
due_date = datetime.now() + timedelta(minutes=10)
|
||||||
|
expiry = int(due_date.timestamp())
|
||||||
|
payload = {
|
||||||
|
"iss": app_id,
|
||||||
|
"sub": app_id,
|
||||||
|
"exp": expiry,
|
||||||
|
"iat": current_timestamp,
|
||||||
|
"aud": "https://github.com/login/oauth/access_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
priv_rsakey = load_pem_private_key(secret, None, default_backend())
|
||||||
|
token = jwt.encode(payload, priv_rsakey, algorithm="RS256")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_metadata(installation_id):
|
||||||
|
token = get_jwt_token()
|
||||||
|
|
||||||
|
url = f"https://api.github.com/app/installations/{installation_id}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers).json()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_repos(access_tokens_url, repositories_url):
|
||||||
|
token = get_jwt_token()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth_response = requests.post(
|
||||||
|
access_tokens_url,
|
||||||
|
headers=headers,
|
||||||
|
).json()
|
||||||
|
|
||||||
|
oauth_token = oauth_response.get("token")
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer " + oauth_token,
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
response = requests.get(
|
||||||
|
repositories_url,
|
||||||
|
headers=headers,
|
||||||
|
).json()
|
||||||
|
return response
|
12
app.json
12
app.json
@ -6,8 +6,16 @@
|
|||||||
"website": "https://plane.so/",
|
"website": "https://plane.so/",
|
||||||
"success_url": "/",
|
"success_url": "/",
|
||||||
"stack": "heroku-22",
|
"stack": "heroku-22",
|
||||||
"keywords": ["plane", "project management", "django", "next"],
|
"keywords": [
|
||||||
"addons": ["heroku-postgresql:mini", "heroku-redis:mini"],
|
"plane",
|
||||||
|
"project management",
|
||||||
|
"django",
|
||||||
|
"next"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"heroku-postgresql:mini",
|
||||||
|
"heroku-redis:mini"
|
||||||
|
],
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
{
|
{
|
||||||
"url": "https://github.com/heroku/heroku-buildpack-python.git"
|
"url": "https://github.com/heroku/heroku-buildpack-python.git"
|
||||||
|
41
apps/app/components/popup/index.tsx
Normal file
41
apps/app/components/popup/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
|
const OAuthPopUp = ({ workspaceSlug, integration }: any) => {
|
||||||
|
const popup = useRef<any>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const checkPopup = () => {
|
||||||
|
const check = setInterval(() => {
|
||||||
|
if (!popup || popup.current.closed || popup.current.closed === undefined) {
|
||||||
|
clearInterval(check);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPopup = () => {
|
||||||
|
const width = 600,
|
||||||
|
height = 600;
|
||||||
|
const left = window.innerWidth / 2 - width / 2;
|
||||||
|
const top = window.innerHeight / 2 - height / 2;
|
||||||
|
const url = `https://github.com/apps/${process.env.NEXT_PUBLIC_GITHUB_APP_NAME}/installations/new?state=${workspaceSlug}`;
|
||||||
|
|
||||||
|
return window.open(url, "", `width=${width}, height=${height}, top=${top}, left=${left}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAuth = () => {
|
||||||
|
popup.current = openPopup();
|
||||||
|
checkPopup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<button onClick={startAuth}>{integration.title}</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OAuthPopUp;
|
@ -1,8 +1,11 @@
|
|||||||
export const CURRENT_USER = "CURRENT_USER";
|
export const CURRENT_USER = "CURRENT_USER";
|
||||||
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS";
|
||||||
export const USER_WORKSPACES = "USER_WORKSPACES";
|
export const USER_WORKSPACES = "USER_WORKSPACES";
|
||||||
|
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
||||||
|
|
||||||
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug}`;
|
export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug}`;
|
||||||
|
export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) =>
|
||||||
|
`WORKSPACE_INTEGRATIONS_${workspaceSlug}`;
|
||||||
|
|
||||||
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`;
|
export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`;
|
||||||
export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) =>
|
export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) =>
|
||||||
|
@ -61,6 +61,10 @@ const workspaceLinks: (wSlug: string) => Array<{
|
|||||||
label: "Billing & Plans",
|
label: "Billing & Plans",
|
||||||
href: `/${workspaceSlug}/settings/billing`,
|
href: `/${workspaceSlug}/settings/billing`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/${workspaceSlug}/settings/integrations`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const sidebarLinks: (
|
const sidebarLinks: (
|
||||||
@ -94,6 +98,10 @@ const sidebarLinks: (
|
|||||||
label: "Labels",
|
label: "Labels",
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
href: `/${workspaceSlug}/projects/${projectId}/settings/labels`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Integrations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const AppLayout: FC<AppLayoutProps> = ({
|
const AppLayout: FC<AppLayoutProps> = ({
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// lib
|
||||||
|
import { requiredAdmin } from "lib/auth";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
import projectService from "services/project.service";
|
||||||
|
|
||||||
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
// types
|
||||||
|
import { IProject, IWorkspace } from "types";
|
||||||
|
import type { NextPageContext, NextPage } from "next";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type TProjectIntegrationsProps = {
|
||||||
|
isMember: boolean;
|
||||||
|
isOwner: boolean;
|
||||||
|
isViewer: boolean;
|
||||||
|
isGuest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: Partial<IProject> = {
|
||||||
|
project_lead: null,
|
||||||
|
default_assignee: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectIntegrations: NextPage<TProjectIntegrationsProps> = (props) => {
|
||||||
|
const { isMember, isOwner, isViewer, isGuest } = props;
|
||||||
|
const [userRepos, setUserRepos] = useState([]);
|
||||||
|
const [activeIntegrationId, setActiveIntegrationId] = useState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
query: { workspaceSlug, projectId },
|
||||||
|
} = useRouter();
|
||||||
|
|
||||||
|
const { data: projectDetails } = useSWR<IProject>(
|
||||||
|
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||||
|
workspaceSlug && projectId
|
||||||
|
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: integrations } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null,
|
||||||
|
() =>
|
||||||
|
workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null
|
||||||
|
);
|
||||||
|
const handleChange = (repo: any) => {
|
||||||
|
const {
|
||||||
|
html_url,
|
||||||
|
owner: { login },
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
} = repo;
|
||||||
|
|
||||||
|
projectService
|
||||||
|
.syncGiuthubRepository(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
activeIntegrationId as any,
|
||||||
|
{ name, owner: login, repository_id: id, url: html_url }
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
console.log(userRepos);
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
settingsLayout="project"
|
||||||
|
memberType={{ isMember, isOwner, isViewer, isGuest }}
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${projectDetails?.name ?? "Project"}`}
|
||||||
|
link={`/${workspaceSlug}/projects/${projectId}/issues`}
|
||||||
|
/>
|
||||||
|
<BreadcrumbItem title="Integrations" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section className="space-y-8">
|
||||||
|
{integrations?.map((integration: any) => (
|
||||||
|
<div
|
||||||
|
key={integration.id}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveIntegrationId(integration.id);
|
||||||
|
projectService
|
||||||
|
.getGithubRepositories(workspaceSlug as any, integration.id)
|
||||||
|
.then((response) => {
|
||||||
|
setUserRepos(response.repositories);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{integration.integration_detail.provider}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{userRepos.length > 0 && (
|
||||||
|
<select
|
||||||
|
onChange={(e) => {
|
||||||
|
const repo = userRepos.find((repo: any) => repo.id == e.target.value);
|
||||||
|
handleChange(repo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={undefined}>Select Repository</option>
|
||||||
|
{userRepos?.map((repo: any) => (
|
||||||
|
<option value={repo.id} key={repo.id}>
|
||||||
|
{repo.full_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = async (ctx: NextPageContext) => {
|
||||||
|
const projectId = ctx.query.projectId as string;
|
||||||
|
const workspaceSlug = ctx.query.workspaceSlug as string;
|
||||||
|
|
||||||
|
const memberDetail = await requiredAdmin(workspaceSlug, projectId, ctx.req?.headers.cookie);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isOwner: memberDetail?.role === 20,
|
||||||
|
isMember: memberDetail?.role === 15,
|
||||||
|
isViewer: memberDetail?.role === 10,
|
||||||
|
isGuest: memberDetail?.role === 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectIntegrations;
|
93
apps/app/pages/[workspaceSlug]/settings/integrations.tsx
Normal file
93
apps/app/pages/[workspaceSlug]/settings/integrations.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// lib
|
||||||
|
import type { NextPage, GetServerSideProps } from "next";
|
||||||
|
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||||
|
// constants
|
||||||
|
// services
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
|
// ui
|
||||||
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
|
import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys";
|
||||||
|
import OAuthPopUp from "components/popup";
|
||||||
|
|
||||||
|
type TWorkspaceIntegrationsProps = {
|
||||||
|
isOwner: boolean;
|
||||||
|
isMember: boolean;
|
||||||
|
isViewer: boolean;
|
||||||
|
isGuest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkspaceIntegrations: NextPage<TWorkspaceIntegrationsProps> = (props) => {
|
||||||
|
const {
|
||||||
|
query: { workspaceSlug },
|
||||||
|
} = useRouter();
|
||||||
|
|
||||||
|
const { data: activeWorkspace } = useSWR(
|
||||||
|
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
|
||||||
|
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: integrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () =>
|
||||||
|
workspaceSlug ? workspaceService.getIntegrations() : null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppLayout
|
||||||
|
settingsLayout="workspace"
|
||||||
|
memberType={props}
|
||||||
|
breadcrumbs={
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem
|
||||||
|
title={`${activeWorkspace?.name ?? "Workspace"}`}
|
||||||
|
link={`/${workspaceSlug}`}
|
||||||
|
/>
|
||||||
|
<BreadcrumbItem title="Integrations" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<section className="space-y-8">
|
||||||
|
{integrations?.map((integration: any) => (
|
||||||
|
<OAuthPopUp
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
key={integration.id}
|
||||||
|
integration={integration}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</AppLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
|
const workspaceSlug = ctx.params?.workspaceSlug as string;
|
||||||
|
|
||||||
|
const memberDetail = await requiredWorkspaceAdmin(workspaceSlug, ctx.req.headers.cookie);
|
||||||
|
|
||||||
|
if (memberDetail === null) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isOwner: memberDetail?.role === 20,
|
||||||
|
isMember: memberDetail?.role === 15,
|
||||||
|
isViewer: memberDetail?.role === 10,
|
||||||
|
isGuest: memberDetail?.role === 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkspaceIntegrations;
|
41
apps/app/pages/installations/[provider]/index.tsx
Normal file
41
apps/app/pages/installations/[provider]/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import appinstallationsService from "services/appinstallations.service";
|
||||||
|
|
||||||
|
interface IGithuPostInstallationProps {
|
||||||
|
installation_id: string;
|
||||||
|
setup_action: string;
|
||||||
|
state: string;
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppPostInstallation = ({
|
||||||
|
installation_id,
|
||||||
|
setup_action,
|
||||||
|
state,
|
||||||
|
provider,
|
||||||
|
}: IGithuPostInstallationProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (state && installation_id) {
|
||||||
|
appinstallationsService
|
||||||
|
.addGithubApp(state, provider, { installation_id })
|
||||||
|
.then((res) => {
|
||||||
|
window.opener = null;
|
||||||
|
window.open("", "_self");
|
||||||
|
window.close();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [state, installation_id, provider]);
|
||||||
|
return <>Loading...</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
console.log(context.query);
|
||||||
|
return {
|
||||||
|
props: context.query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppPostInstallation;
|
20
apps/app/services/appinstallations.service.ts
Normal file
20
apps/app/services/appinstallations.service.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// services
|
||||||
|
import APIService from "services/api.service";
|
||||||
|
|
||||||
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
|
class AppInstallationsService extends APIService {
|
||||||
|
constructor() {
|
||||||
|
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGithubApp(workspaceSlug: string, provider: string, data: any): Promise<any> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/workspace-integrations/${provider}/`, data)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AppInstallationsService();
|
@ -201,6 +201,37 @@ class ProjectServices extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getGithubRepositories(slug: string, workspaceIntegrationId: string): Promise<any> {
|
||||||
|
return this.get(
|
||||||
|
`/api/workspaces/${slug}/workspace-integrations/${workspaceIntegrationId}/github-repositories/`
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncGiuthubRepository(
|
||||||
|
slug: string,
|
||||||
|
projectId: string,
|
||||||
|
workspaceIntegrationId: string,
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
repository_id: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
return this.post(
|
||||||
|
`/api/workspaces/${slug}/projects/${projectId}/workspace-integrations/${workspaceIntegrationId}/github-repository-sync/`,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProjectServices();
|
export default new ProjectServices();
|
||||||
|
@ -169,6 +169,20 @@ class WorkspaceService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async getIntegrations(): Promise<any> {
|
||||||
|
return this.get(`/api/integrations/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getWorkspaceIntegrations(slug: string): Promise<any> {
|
||||||
|
return this.get(`/api/workspaces/${slug}/workspace-integrations/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WorkspaceService();
|
export default new WorkspaceService();
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
"SENTRY_AUTH_TOKEN",
|
"SENTRY_AUTH_TOKEN",
|
||||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||||
|
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
||||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||||
"NEXT_PUBLIC_ENABLE_OAUTH"
|
"NEXT_PUBLIC_ENABLE_OAUTH"
|
||||||
],
|
],
|
||||||
|
Loading…
Reference in New Issue
Block a user