mirror of https://github.com/ctk-hq/ctk
React initial rewrite, wip.
parent
a1b58c5e6d
commit
7bc6892865
@ -1,5 +0,0 @@
|
||||
.git
|
||||
Dockerfile
|
||||
.DS_Store
|
||||
README.md
|
||||
env.*
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"editor.lineHeight": 20,
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.renderWhitespace": "all",
|
||||
"editor.renderControlCharacters": true,
|
||||
"files.eol": "\n",
|
||||
"python.formatting.provider": "black"
|
||||
}
|
||||
@ -1,48 +1,49 @@
|
||||
BACKEND_CONTAINER_NAME = nuxx-api
|
||||
NGINX_CONTAINER_NAME = nuxx-nginx
|
||||
|
||||
.PHONY : validate build pull up down down_clean reset run backend_dev shell_backend shell_nginx local_setup local_build
|
||||
|
||||
validate :
|
||||
docker-compose config
|
||||
|
||||
build : validate
|
||||
docker-compose build
|
||||
|
||||
pull :
|
||||
docker-compose pull
|
||||
|
||||
up :
|
||||
@ docker-compose up -d
|
||||
|
||||
down :
|
||||
docker-compose down
|
||||
|
||||
down_clean : down
|
||||
-docker volume rm nuxx_postgres_data
|
||||
-docker volume rm nuxx_django_static
|
||||
|
||||
reset : down
|
||||
make up
|
||||
|
||||
run : validate
|
||||
docker-compose run $(BACKEND_CONTAINER_NAME) -c "cd /home/app/ && python manage.py runserver 0.0.0.0:9001"
|
||||
|
||||
dev_backend :
|
||||
docker exec -ti $(BACKEND_CONTAINER_NAME) python /home/app/manage.py runserver 0.0.0.0:9001
|
||||
|
||||
shell_backend:
|
||||
docker exec -it ${BACKEND_CONTAINER_NAME} bash
|
||||
|
||||
shell_nginx:
|
||||
docker exec -it ${NGINX_CONTAINER_NAME} bash
|
||||
|
||||
local_build:
|
||||
@ cd ./src/composer && npm install && npm run build_local
|
||||
|
||||
local_setup: local_build up
|
||||
@ echo "Waiting for PostgreSQL..." \
|
||||
&& sleep 5 \
|
||||
&& docker exec -it ${BACKEND_CONTAINER_NAME} python /home/app/manage.py makemigrations \
|
||||
&& docker exec -it ${BACKEND_CONTAINER_NAME} python /home/app/manage.py migrate \
|
||||
&& docker exec -it ${BACKEND_CONTAINER_NAME} python /home/app/manage.py collectstatic --noinput
|
||||
ORGANIZATION = corpulent
|
||||
CONTAINER = ctk-server
|
||||
VERSION = 0.1.0
|
||||
|
||||
.PHONY : validate build pull up down down_clean reset run backend_dev shell_server shell_nginx local_setup local_build
|
||||
|
||||
validate :
|
||||
docker-compose config
|
||||
|
||||
build : validate
|
||||
docker-compose build
|
||||
|
||||
pull :
|
||||
docker-compose pull
|
||||
|
||||
up :
|
||||
docker-compose up -d
|
||||
|
||||
up_local :
|
||||
docker-compose up -d --no-build
|
||||
|
||||
down :
|
||||
docker-compose down
|
||||
|
||||
down_clean : down
|
||||
-docker volume rm ctk_postgres_data
|
||||
-docker volume rm ctk_django_static
|
||||
|
||||
reset : down
|
||||
make up
|
||||
|
||||
run_server : validate
|
||||
docker-compose run $(CONTAINER) -c "cd /home/server/ && python manage.py runserver 0.0.0.0:9001"
|
||||
|
||||
dev_server :
|
||||
docker exec -ti $(CONTAINER) python /home/server/manage.py runserver 0.0.0.0:9001
|
||||
|
||||
shell_server:
|
||||
docker exec -it ${CONTAINER} bash
|
||||
|
||||
frontend_build:
|
||||
@ cd ./services/frontend/src && npm install && npm run build
|
||||
|
||||
local_setup: frontend_build up
|
||||
@ echo "Waiting for PostgreSQL..." \
|
||||
&& sleep 5 \
|
||||
&& docker exec -it ${CONTAINER} python /home/server/manage.py makemigrations \
|
||||
&& docker exec -it ${CONTAINER} python /home/server/manage.py migrate \
|
||||
&& docker exec -it ${BACKEND_CONTAINER_NAME} python /home/server/manage.py collectstatic --noinput
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
#AWS_STORAGE_BUCKET_NAME=
|
||||
DB_REMOTE=False
|
||||
#DB_HOST=
|
||||
#DB_NAME=
|
||||
#DB_USER=
|
||||
#DB_PASS=
|
||||
#SOCIAL_AUTH_CUSTOM_CALLBACK=
|
||||
#SOCIAL_AUTH_CUSTOM_CALLBACK_PAGE=
|
||||
Binary file not shown.
@ -0,0 +1,44 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
software-properties-common \
|
||||
build-essential
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
postgresql \
|
||||
postgresql-contrib \
|
||||
wget \
|
||||
nano \
|
||||
curl \
|
||||
lsof \
|
||||
supervisor && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd uwsgi && adduser uwsgi root
|
||||
RUN useradd supervisor && adduser supervisor root
|
||||
|
||||
COPY ./services/backend/requirements.txt ./requirements.txt
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install -r ./requirements.txt && \
|
||||
rm ./requirements.txt
|
||||
|
||||
RUN touch /var/log/backend_out.log && \
|
||||
touch /var/log/django.log
|
||||
|
||||
RUN chmod g+w -R /var/log/
|
||||
|
||||
EXPOSE 9000 9001
|
||||
|
||||
COPY ./services/backend/src ./server
|
||||
COPY ./services/backend/configs/supervisor/api.conf /etc/supervisor/conf.d/api.conf
|
||||
COPY ./services/backend/configs/uwsgi ./config/uwsgi
|
||||
|
||||
RUN rm -rf /tmp/uwsgi && \
|
||||
mkdir -p /tmp/uwsgi && \
|
||||
ln -s ./config/uwsgi/uwsgi.ini /tmp/uwsgi/
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
@ -1,6 +1,6 @@
|
||||
ORGANIZATION = nuxxapp
|
||||
CONTAINER = nuxx-api
|
||||
VERSION = 1.0.0
|
||||
ORGANIZATION = agolub
|
||||
CONTAINER = ctk-server
|
||||
VERSION = 0.1.0
|
||||
|
||||
.PHONY: build
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
|
||||
[program:app]
|
||||
priority=1
|
||||
user = uwsgi
|
||||
command = /usr/local/bin/uwsgi --ini /tmp/uwsgi/uwsgi.ini
|
||||
autorestart=false
|
||||
@ -0,0 +1,26 @@
|
||||
django==4.0.4
|
||||
django-cors-headers==3.11.0
|
||||
django-axes==5.32.0
|
||||
djangorestframework==3.13.1
|
||||
djangorestframework-simplejwt==5.1.0
|
||||
drf-extensions==0.7.1
|
||||
dj-rest-auth[with_social]==2.2.4
|
||||
|
||||
psycopg[binary]==3.0.12
|
||||
psycopg==3.0.12
|
||||
psycopg2-binary==2.9.3
|
||||
uwsgi==2.0.20
|
||||
botocore==1.24.46
|
||||
boto3==1.21.46
|
||||
Jinja2==3.1.1
|
||||
validators==0.19.0
|
||||
requests==2.27.1
|
||||
celery==5.2.3
|
||||
redis==4.3.1
|
||||
simple-salesforce==1.11.6
|
||||
cryptography==37.0.2
|
||||
chardet==4.0.0
|
||||
pyaml==21.10.1
|
||||
docker==5.0.3
|
||||
ruamel.yaml==0.17.21
|
||||
better-profanity==0.7.0
|
||||
@ -0,0 +1,14 @@
|
||||
from django.contrib import admin
|
||||
from .models import Project
|
||||
|
||||
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'uuid',
|
||||
'created_at',
|
||||
'updated_at')
|
||||
|
||||
|
||||
admin.site.register(Project, ProjectAdmin)
|
||||
@ -0,0 +1,9 @@
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
from organizations.utils import get_user_org
|
||||
|
||||
|
||||
class FilterByOrg(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
org = get_user_org(request.user)
|
||||
queryset_filters = {"org": org}
|
||||
return queryset.filter(**queryset_filters)
|
||||
@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.0.4 on 2022-06-22 10:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('organizations', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Project',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='Untitled', max_length=500)),
|
||||
('data', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='organizations.organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project',
|
||||
'verbose_name_plural': 'Projects',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,5 @@
|
||||
from .project import Project
|
||||
|
||||
__all__ = [
|
||||
"Project"
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
from django.db import models
|
||||
from organizations.models import Organization
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
org = models.ForeignKey(
|
||||
Organization,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="projects",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
name = models.CharField(max_length=500, blank=False, null=False, default="Untitled")
|
||||
uuid = models.CharField(max_length=500, blank=True, null=True, unique=True)
|
||||
data = models.TextField(blank=False)
|
||||
created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
|
||||
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project"
|
||||
verbose_name_plural = "Projects"
|
||||
ordering = ["-created_at"]
|
||||
@ -0,0 +1,23 @@
|
||||
from django.urls import include, path
|
||||
|
||||
from rest_framework_extensions.routers import ExtendedDefaultRouter
|
||||
|
||||
from .views import project, generate, user, view
|
||||
|
||||
|
||||
class DefaultRouterPlusPlus(ExtendedDefaultRouter):
|
||||
"""DefaultRouter with optional trailing slash and drf-extensions nesting."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.trailing_slash = r"/?"
|
||||
|
||||
api_urls = [
|
||||
path("", view.ViewGenericAPIView.as_view()),
|
||||
path("projects/", project.ProjectListCreateAPIView.as_view()),
|
||||
path("projects/<str:uuid>/", project.ProjectGenericAPIView.as_view()),
|
||||
path("generate/", generate.GenerateGenericAPIView.as_view()),
|
||||
path("auth/self/", user.UserGenericAPIView.as_view()),
|
||||
path("auth/", include("dj_rest_auth.urls")),
|
||||
path("auth/registration/", include("dj_rest_auth.registration.urls")),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
import json
|
||||
from rest_framework import serializers
|
||||
from .models import Project
|
||||
|
||||
|
||||
class DataField(serializers.Field):
|
||||
def to_representation(self, value):
|
||||
return value
|
||||
|
||||
def to_internal_value(self, value):
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
data = DataField()
|
||||
class Meta(object):
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class UserSelfSerializer(serializers.Serializer):
|
||||
pk = serializers.IntegerField()
|
||||
username = serializers.CharField(max_length=200)
|
||||
first_name = serializers.CharField(max_length=200)
|
||||
last_name = serializers.CharField(max_length=200)
|
||||
email = serializers.CharField(max_length=200)
|
||||
@ -0,0 +1,35 @@
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .utils import generate_dc
|
||||
|
||||
|
||||
class GenerateGenericAPIView(generics.GenericAPIView):
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def post(self, request, format=None):
|
||||
request_data = request.data
|
||||
version = request_data['data'].get('version', '3')
|
||||
services = request_data['data'].get('services', None)
|
||||
connections = request_data['data'].get('connections', None)
|
||||
volumes = request_data['data'].get('volumes', None)
|
||||
networks = request_data['data'].get('networks', None)
|
||||
secrets = request_data['data'].get('secrets', None)
|
||||
configs = request_data['data'].get('configs', None)
|
||||
|
||||
code = generate_dc(
|
||||
services,
|
||||
connections,
|
||||
volumes,
|
||||
networks,
|
||||
secrets,
|
||||
configs,
|
||||
version=version,
|
||||
return_format='yaml')
|
||||
resp = {'code': code}
|
||||
|
||||
|
||||
return Response(resp, status=status.HTTP_200_OK)
|
||||
@ -0,0 +1,93 @@
|
||||
import uuid
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.serializers import ProjectSerializer
|
||||
from api.models import Project
|
||||
from api.filters import FilterByOrg
|
||||
|
||||
from organizations.utils import get_user_org
|
||||
|
||||
from .utils import get_project_obj_by_uuid
|
||||
|
||||
|
||||
class ProjectListCreateAPIView(generics.ListCreateAPIView):
|
||||
permission_classes = []
|
||||
serializer_class = ProjectSerializer
|
||||
queryset = Project.objects.all()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
filter_backends = (FilterByOrg,)
|
||||
|
||||
for backend in list(filter_backends):
|
||||
queryset = backend().filter_queryset(self.request, queryset, view=self)
|
||||
|
||||
return queryset
|
||||
|
||||
def list(self, request):
|
||||
try:
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
org = None if request.user.is_anonymous else get_user_org(request.user)
|
||||
data = {
|
||||
"uuid": str(uuid.uuid4())[:10],
|
||||
**request.data
|
||||
}
|
||||
|
||||
if org:
|
||||
data["org"] = org.pk
|
||||
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
|
||||
class ProjectGenericAPIView(generics.GenericAPIView):
|
||||
permission_classes = []
|
||||
serializer_class = ProjectSerializer
|
||||
queryset = Project.objects.all()
|
||||
|
||||
def get(self, request, uuid):
|
||||
try:
|
||||
org = None if request.user.is_anonymous else get_user_org(request.user)
|
||||
if project_obj := get_project_obj_by_uuid(uuid):
|
||||
return Response(ProjectSerializer(project_obj).data)
|
||||
except Exception as e:
|
||||
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def put(self, request, uuid):
|
||||
org = None if request.user.is_anonymous else get_user_org(request.user)
|
||||
if project_obj := get_project_obj_by_uuid(uuid):
|
||||
data = request.data
|
||||
serializer = ProjectSerializer(project_obj, data=data)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def delete(self, request, uuid):
|
||||
org = None if request.user.is_anonymous else get_user_org(request.user)
|
||||
if project_obj := get_project_obj_by_uuid(uuid):
|
||||
project_obj.delete()
|
||||
return Response({}, status=status.HTTP_204_NO_CONTENT)
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
@ -0,0 +1,12 @@
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.serializers import UserSelfSerializer
|
||||
|
||||
|
||||
class UserGenericAPIView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
return Response(UserSelfSerializer(request.user).data)
|
||||
@ -0,0 +1,9 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, status
|
||||
|
||||
|
||||
class ViewGenericAPIView(generics.GenericAPIView):
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
@ -1,16 +1,16 @@
|
||||
"""
|
||||
ASGI config for server project.
|
||||
ASGI config.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@ -0,0 +1,197 @@
|
||||
"""
|
||||
Django settings.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure--i+bd*fda@!=_0yv$((3(@nruqvv(8c1c8no^+yjl%@b859f57"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
CORS_URLS_REGEX = r"^/v1/.*$"
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.sites",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"dj_rest_auth",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"dj_rest_auth.registration",
|
||||
"corsheaders",
|
||||
"axes",
|
||||
"organizations",
|
||||
"api",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"axes.middleware.AxesMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "main.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "main.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DB_REMOTE = os.environ.get("DB_REMOTE", "False").lower() == "true"
|
||||
|
||||
if DB_REMOTE:
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": os.getenv("DB_HOST", None),
|
||||
"NAME": os.getenv("DB_NAME", None),
|
||||
"USER": os.getenv("DB_USER", None),
|
||||
"PASSWORD": os.getenv("DB_PASS", None),
|
||||
"PORT": 5432,
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "postgres",
|
||||
"NAME": "postgres",
|
||||
"USER": "postgres",
|
||||
"PASSWORD": "postgres",
|
||||
"PORT": 5432,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"axes.backends.AxesBackend",
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"dj_rest_auth.jwt_auth.JWTCookieAuthentication",
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
|
||||
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
|
||||
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
|
||||
"PAGE_SIZE": 300,
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "json",
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
|
||||
"rest_framework.renderers.BrowsableAPIRenderer"
|
||||
)
|
||||
|
||||
|
||||
# allauth
|
||||
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||||
ACCOUNT_PRESERVE_USERNAME_CASING = False
|
||||
|
||||
# dj_rest_auth
|
||||
REST_USE_JWT = True
|
||||
|
||||
# simple jwt
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(days=15),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import URLPattern, include, path
|
||||
|
||||
from api.routing import api_urls
|
||||
|
||||
|
||||
def opt_slash_path(route: str, view: Callable, name: Optional[str] = None) -> URLPattern:
|
||||
"""Catches path with or without trailing slash, taking into account query param and hash."""
|
||||
# Ignoring the type because while name can be optional on re_path, mypy doesn't agree
|
||||
return re_path(fr"^{route}/?(?:[?#].*)?$", view, name=name) # type: ignore
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("v1/", include(api_urls)),
|
||||
]
|
||||
@ -1,16 +1,16 @@
|
||||
"""
|
||||
WSGI config for server project.
|
||||
WSGI config.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@ -0,0 +1,9 @@
|
||||
from django.contrib import admin
|
||||
from .models import Organization
|
||||
|
||||
|
||||
class OrganizationAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "name", "created_at", "updated_at")
|
||||
|
||||
|
||||
admin.site.register(Organization, OrganizationAdmin)
|
||||
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OrganizationsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'organizations'
|
||||
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.0.4 on 2022-05-31 14:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('users', models.ManyToManyField(related_name='orgs', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization',
|
||||
'verbose_name_plural': 'Organizations',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,50 @@
|
||||
from allauth.account.signals import user_signed_up, user_logged_in
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField(max_length=255, blank=True, null=True)
|
||||
users = models.ManyToManyField(User, related_name="orgs")
|
||||
created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True)
|
||||
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Organization"
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def total_members(self):
|
||||
return self.users.count()
|
||||
|
||||
def is_member(self, user):
|
||||
return user in self.users.all()
|
||||
|
||||
def add_user(self, user):
|
||||
return self.users.add(user)
|
||||
|
||||
def remove_user(self, user):
|
||||
return self.users.remove(user)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
@receiver(user_signed_up)
|
||||
def handler(sender, request, user, **kwargs):
|
||||
org_name = f"{user.username.lower()}-org"
|
||||
org = Organization.objects.create(name=org_name)
|
||||
org.add_user(user)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=User)
|
||||
def handler(instance, **kwargs):
|
||||
for org in instance.orgs.all():
|
||||
org.remove_user(instance)
|
||||
if org.total_members() == 0:
|
||||
org.delete()
|
||||
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -0,0 +1,4 @@
|
||||
def get_user_org(user):
|
||||
user_orgs = user.orgs.all()
|
||||
if user_orgs.count():
|
||||
return user_orgs[0]
|
||||
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@ -0,0 +1 @@
|
||||
REACT_APP_API_SERVER=http://localhost:9001/v1
|
||||
@ -0,0 +1 @@
|
||||
REACT_APP_API_SERVER=http://localhost:9001/v1
|
||||
@ -0,0 +1,12 @@
|
||||
FROM node:16 as build
|
||||
|
||||
WORKDIR /build
|
||||
COPY ./services/frontend/ .
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=build /build/build /usr/share/nginx/html
|
||||
COPY --from=build /build/configs/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "ctk-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^0.19.0",
|
||||
"@codemirror/closebrackets": "^0.19.0",
|
||||
"@codemirror/commands": "^0.19.0",
|
||||
"@codemirror/comment": "^0.19.0",
|
||||
"@codemirror/fold": "^0.19.0",
|
||||
"@codemirror/history": "^0.19.0",
|
||||
"@codemirror/lang-json": "^0.19.2",
|
||||
"@codemirror/lang-python": "^0.19.0",
|
||||
"@codemirror/legacy-modes": "^0.19.0",
|
||||
"@codemirror/lint": "^0.19.0",
|
||||
"@codemirror/matchbrackets": "^0.19.0",
|
||||
"@codemirror/rectangular-selection": "^0.19.0",
|
||||
"@codemirror/search": "^0.19.0",
|
||||
"@codemirror/stream-parser": "^0.19.9",
|
||||
"@codemirror/view": "^0.19.0",
|
||||
"@headlessui/react": "^1.6.4",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@jsplumb/browser-ui": "^5.5.2",
|
||||
"@jsplumb/common": "^5.5.2",
|
||||
"@jsplumb/connector-bezier": "^5.5.2",
|
||||
"@jsplumb/core": "^5.5.2",
|
||||
"@jsplumb/util": "^5.5.2",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.1",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"codemirror": "^5.65.5",
|
||||
"d3": "^7.3.0",
|
||||
"formik": "^2.2.9",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-toast": "^2.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"tailwindcss": "^3.0.19",
|
||||
"typescript": "^4.5.5",
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^2.1.4",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.1.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^16.11.22",
|
||||
"@types/react": "^17.0.45",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"postcss": "^8.4.6"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Visual Argo Workflows",
|
||||
"name": "Visual Argo Workflows",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@ -0,0 +1,84 @@
|
||||
import { useReducer, useEffect } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { LOCAL_STORAGE } from "./constants";
|
||||
import { reducer, initialState } from "./reducers";
|
||||
import { useLocalStorageAuth } from "./hooks/auth";
|
||||
import { checkHttpStatus } from "./services/helpers";
|
||||
import { authSelf } from "./reducers";
|
||||
import { refresh, self } from "./services/auth";
|
||||
|
||||
import Project from "./components/Project";
|
||||
import Profile from "./components/Profile";
|
||||
import Signup from "./components/Auth/Signup";
|
||||
import Login from "./components/Auth/Login";
|
||||
|
||||
import { ProtectedRouteProps } from "./partials/ProtectedRoute";
|
||||
import ProtectedRoute from "./partials/ProtectedRoute";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
export default function App() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const auth = useLocalStorageAuth();
|
||||
const isAuthenticated = !!(auth && Object.keys(auth).length);
|
||||
|
||||
const defaultProtectedRouteProps: Omit<ProtectedRouteProps, 'outlet'> = {
|
||||
isAuthenticated: isAuthenticated,
|
||||
authenticationPath: '/login',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
self()
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
dispatch(authSelf(data));
|
||||
})
|
||||
.catch(err => {
|
||||
// since auth is set in localstorage,
|
||||
// try to refresh the existing token,
|
||||
// on error clear localstorage
|
||||
if (err.status === 401) {
|
||||
err.text().then((text: string) => {
|
||||
const textObj = JSON.parse(text);
|
||||
if (textObj.code === "user_not_found") {
|
||||
localStorage.removeItem(LOCAL_STORAGE);
|
||||
}
|
||||
})
|
||||
|
||||
refresh()
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
const localData = localStorage.getItem(LOCAL_STORAGE);
|
||||
|
||||
if (localData) {
|
||||
const localDataParsed = JSON.parse(localData);
|
||||
if (localDataParsed && Object.keys(localDataParsed).length) {
|
||||
localDataParsed.access_token = data.access;
|
||||
localStorage.setItem(LOCAL_STORAGE, JSON.stringify(localDataParsed))
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
localStorage.removeItem(LOCAL_STORAGE);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [dispatch, isAuthenticated]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toaster />
|
||||
<Routes>
|
||||
<Route path="/" element={<Project dispatch={dispatch} state={state} />} />
|
||||
<Route path="/projects/:uuid" element={<Project dispatch={dispatch} state={state} />} />
|
||||
|
||||
<Route path="/profile" element={<ProtectedRoute {...defaultProtectedRouteProps} outlet={<Profile dispatch={dispatch} state={state} />} />} />
|
||||
<Route path="/signup" element={<Signup dispatch={dispatch} />} />
|
||||
<Route path="/login" element={<Login dispatch={dispatch} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
import { useState } from "react";
|
||||
import { useFormik } from "formik";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import DarkModeSwitch from "../../../components/DarkModeSwitch";
|
||||
import Spinner from "../../../components/Spinner";
|
||||
import { toaster } from "../../../utils";
|
||||
import { checkHttpStatus } from "../../../services/helpers";
|
||||
import { logIn } from "../../../services/auth";
|
||||
import { LOCAL_STORAGE } from "../../../constants";
|
||||
import { authLoginSuccess } from "../../../reducers";
|
||||
|
||||
interface IProfileProps {
|
||||
dispatch: any;
|
||||
}
|
||||
|
||||
const Login = (props: IProfileProps) => {
|
||||
const { dispatch } = props;
|
||||
const [loggingIn, setLoggingIn] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: ""
|
||||
},
|
||||
onSubmit: values => {
|
||||
const username = values.username;
|
||||
const password = values.password;
|
||||
setLoggingIn(true);
|
||||
if (username && password) {
|
||||
logIn(username, password)
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
localStorage.setItem(LOCAL_STORAGE, JSON.stringify({
|
||||
"access_token": data.access_token,
|
||||
"refresh_token": data.refresh_token
|
||||
}))
|
||||
dispatch(authLoginSuccess(data))
|
||||
navigate("/");
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) {
|
||||
err.text().then((e: string) => {
|
||||
toaster(e, "error");
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoggingIn(false);
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
|
||||
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-end items-center">
|
||||
<div className="ml-4 flex md:ml-6">
|
||||
<DarkModeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="py-6 md:w-2/3 lg:w-1/4 mx-auto">
|
||||
<h2 className="mb-4 px-4 sm:px-6 md:flex-row md:px-8 text-xl font-extrabold dark:text-white text-gray-900">
|
||||
Sign in
|
||||
</h2>
|
||||
<form autoComplete="off">
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-2
|
||||
`}
|
||||
type="text"
|
||||
placeholder="username"
|
||||
autoComplete="off"
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.username}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-2
|
||||
`}
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autoComplete="new-password"
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end px-4 sm:px-6 md:flex-row md:px-8 mb-2">
|
||||
<button
|
||||
onClick={() => formik.handleSubmit()}
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
|
||||
>
|
||||
<div className="flex justify-center items-center space-x-2">
|
||||
{loggingIn &&
|
||||
<Spinner className="w-5 h-5 text-green-300" />
|
||||
}
|
||||
<span>Login</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="text-center px-4">
|
||||
<Link
|
||||
className="font-medium px-3 dark:text-blue-400 dark:hover:text-blue-500 text-blue-600 hover:text-blue-700"
|
||||
to="/signup"
|
||||
>
|
||||
<span className="text-sm">Create account</span>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@ -0,0 +1,234 @@
|
||||
import { useState } from "react";
|
||||
import { useFormik } from "formik";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import DarkModeSwitch from "../../../components/DarkModeSwitch";
|
||||
import Spinner from "../../../components/Spinner";
|
||||
import { toaster } from "../../../utils";
|
||||
import { checkHttpStatus } from "../../../services/helpers";
|
||||
import { signup } from "../../../services/auth";
|
||||
import { LOCAL_STORAGE } from "../../../constants";
|
||||
import { authLoginSuccess } from "../../../reducers";
|
||||
|
||||
interface IProfileProps {
|
||||
dispatch: any;
|
||||
}
|
||||
|
||||
const Signup = (props: IProfileProps) => {
|
||||
const { dispatch } = props;
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: ""
|
||||
},
|
||||
onSubmit: values => {
|
||||
const username = values.username;
|
||||
const email = values.email;
|
||||
const password1 = values.password;
|
||||
const password2 = values.confirmPassword;
|
||||
|
||||
setSigningUp(true);
|
||||
if (username && email && password1 && password2) {
|
||||
signup(username, email, password1, password2)
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
localStorage.setItem(LOCAL_STORAGE, JSON.stringify({
|
||||
"access_token": data.access_token,
|
||||
"refresh_token": data.refresh_token
|
||||
}))
|
||||
dispatch(authLoginSuccess(data))
|
||||
navigate("/");
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) {
|
||||
err.text().then((e: string) => {
|
||||
toaster(e, "error");
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
setSigningUp(false);
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
|
||||
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-end items-center">
|
||||
<div className="ml-4 flex md:ml-6">
|
||||
<DarkModeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="py-6 md:w-2/3 lg:w-1/4 mx-auto">
|
||||
<h2 className="mb-4 px-4 sm:px-6 md:flex-row md:px-8 text-xl font-extrabold dark:text-white text-gray-900">
|
||||
Create account
|
||||
</h2>
|
||||
<form autoComplete="off">
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-2
|
||||
`}
|
||||
type="text"
|
||||
placeholder="username"
|
||||
autoComplete="off"
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.username}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-2
|
||||
`}
|
||||
type="text"
|
||||
placeholder="email"
|
||||
autoComplete="off"
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-2
|
||||
`}
|
||||
type="password"
|
||||
placeholder="password"
|
||||
autoComplete="new-password"
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 md:flex-row md:px-8">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
dark:bg-gray-900
|
||||
appearance-none
|
||||
w-full
|
||||
block
|
||||
text-gray-700
|
||||
dark:text-white
|
||||
border
|
||||
border-gray-100
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
mb-4
|
||||
`}
|
||||
type="password"
|
||||
placeholder="confirm password"
|
||||
autoComplete="new-password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.confirmPassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end px-4 sm:px-6 md:flex-row md:px-8 mb-4">
|
||||
<button
|
||||
onClick={() => formik.handleSubmit()}
|
||||
type="button"
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-green-600 text-sm font-medium text-white hover:bg-green-700 sm:w-auto text-sm"
|
||||
>
|
||||
<div className="flex justify-center items-center space-x-2">
|
||||
{signingUp &&
|
||||
<Spinner className="w-5 h-5 text-green-300" />
|
||||
}
|
||||
<span>Signup</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="text-center px-4">
|
||||
<Link
|
||||
className="font-medium px-3 dark:text-blue-400 dark:hover:text-blue-500 text-blue-600 hover:text-blue-700"
|
||||
to="/login"
|
||||
>
|
||||
<span className="text-sm">Already have an account?</span>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Signup;
|
||||
@ -0,0 +1,379 @@
|
||||
import { FC, useState, useEffect, createRef, useRef } from "react";
|
||||
import { useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { Dictionary, values } from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import YAML from "yaml";
|
||||
import { PlusIcon } from "@heroicons/react/solid";
|
||||
import {
|
||||
nodeCreated,
|
||||
nodeDeleted,
|
||||
nodeUpdated,
|
||||
connectionDetached,
|
||||
connectionAttached,
|
||||
position
|
||||
} from "../../reducers";
|
||||
import Remove from "../Remove";
|
||||
import ModalServiceCreate from "../Modal/Service/Create";
|
||||
import ModalServiceEdit from "../Modal/Service/Edit";
|
||||
import ModalCreate from "../Modal/Node/Create";
|
||||
import ModalEdit from "../Modal/Node/Edit";
|
||||
import { useClickOutside } from "../../utils/clickOutside";
|
||||
import { IClientNodeItem, IGraphData } from "../../types";
|
||||
import { nodeLibraries } from "../../utils/data/libraries";
|
||||
import { getClientNodeItem, flattenLibraries, ensure } from "../../utils";
|
||||
import { flattenGraphData } from "../../utils/generators";
|
||||
import { generateHttp } from "../../services/generate";
|
||||
import { checkHttpStatus } from "../../services/helpers";
|
||||
import { useJsPlumb } from "../useJsPlumb";
|
||||
import CodeEditor from "../CodeEditor";
|
||||
|
||||
const CANVAS_ID: string = "canvas-container-" + uuidv4();
|
||||
|
||||
interface ICanvasProps {
|
||||
state: any;
|
||||
dispatch: any;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const Canvas: FC<ICanvasProps> = (props) => {
|
||||
const { state, dispatch, height } = props;
|
||||
|
||||
const [language, setLanguage] = useState("yaml");
|
||||
const [scale, setScale] = useState(1);
|
||||
const [generatedCode, setGeneratedCode] = useState<string>();
|
||||
const [formattedCode, setFormattedCode] = useState<string>("");
|
||||
const [instanceNodes, setInstanceNodes] = useState(state.nodes);
|
||||
const [instanceConnections, setInstanceConnections] = useState(state.connections);
|
||||
const [copyText, setCopyText] = useState("Copy");
|
||||
const [selectedNode, setSelectedNode] = useState<IClientNodeItem | null>(null);
|
||||
const [showModalCreateStep, setShowModalCreateStep] = useState(false);
|
||||
const [showModalEditStep, setShowModalEditStep] = useState(false);
|
||||
const [showModalCreate, setShowModalCreate] = useState(false);
|
||||
const [showModalEdit, setShowModalEdit] = useState(false);
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [_scale, _setScale] = useState(1);
|
||||
const [_left, _setLeft] = useState(0);
|
||||
const [_top, _setTop] = useState(0);
|
||||
const [_initX, _setInitX] = useState(0);
|
||||
const [_initY, _setInitY] = useState(0);
|
||||
const [containerCallbackRef, setZoom, setStyle, removeEndpoint] = useJsPlumb(
|
||||
instanceNodes,
|
||||
instanceConnections,
|
||||
((graphData: IGraphData) => onGraphUpdate(graphData)),
|
||||
((positionData: any) => onEndpointPositionUpdate(positionData)),
|
||||
((connectionData: any) => onConnectionAttached(connectionData)),
|
||||
((connectionData: any) => onConnectionDetached(connectionData))
|
||||
);
|
||||
|
||||
const drop = createRef<HTMLDivElement>();
|
||||
const stateRef = useRef<Dictionary<IClientNodeItem>>();
|
||||
|
||||
let translateWidth = (document.documentElement.clientWidth * (1 - scale)) / 2;
|
||||
let translateHeight = ((document.documentElement.clientHeight - 64) * (1 - scale)) / 2;
|
||||
|
||||
stateRef.current = state.nodes;
|
||||
|
||||
useClickOutside(drop, () => {
|
||||
setShowModalCreateStep(false);
|
||||
setShowModalCreate(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setScale(_scale);
|
||||
}, [_scale]);
|
||||
|
||||
const debouncedOnGraphUpdate = useMemo(() => debounce((graphData) => {
|
||||
const flatData = flattenGraphData(graphData);
|
||||
generateHttp(flatData)
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
if (data['code'].length) {
|
||||
for (var i = 0; i < data['code'].length; ++i) {
|
||||
data['code'][i] = data['code'][i].replace(/(\r\n|\n|\r)/gm, "");
|
||||
}
|
||||
|
||||
const code = data['code'].join("\n");
|
||||
setGeneratedCode(code);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
});
|
||||
}, 450), []);
|
||||
|
||||
const debouncedOnCodeChange = useMemo(() => debounce((code: string) => {
|
||||
//formik.setFieldValue("code", e, false);
|
||||
}, 700), []);
|
||||
|
||||
const zoomIn = () => {
|
||||
setScale((scale) => scale + 0.1);
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
setScale((scale) => scale - 0.1);
|
||||
}
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(formattedCode);
|
||||
setCopyText("Copied");
|
||||
|
||||
setTimeout(() => {
|
||||
setCopyText("Copy");
|
||||
}, 300);
|
||||
}
|
||||
|
||||
const onAddEndpoint = (values: any) => {
|
||||
let sections = flattenLibraries(nodeLibraries);
|
||||
let clientNodeItem = getClientNodeItem(values, ensure(sections.find((l) => l.Type === values.type)));
|
||||
clientNodeItem.position = { left: 60, top: 30 };
|
||||
dispatch(nodeCreated(clientNodeItem));
|
||||
}
|
||||
|
||||
const onUpdateEndpoint = (nodeItem: IClientNodeItem) => {
|
||||
dispatch(nodeUpdated(nodeItem));
|
||||
}
|
||||
|
||||
const onRemoveEndpoint = (key: string) => {
|
||||
const nodeToRemove = instanceNodes[key];
|
||||
removeEndpoint(nodeToRemove);
|
||||
dispatch(nodeDeleted(nodeToRemove));
|
||||
}
|
||||
|
||||
const onEndpointPositionUpdate = (positionData: any) => {
|
||||
if (stateRef.current) {
|
||||
const node = stateRef.current[positionData.key];
|
||||
node.position = {...node.position, ...positionData.position};
|
||||
dispatch(nodeUpdated(node));
|
||||
}
|
||||
};
|
||||
|
||||
const onConnectionDetached = (data: any) => {
|
||||
dispatch(connectionDetached(data));
|
||||
}
|
||||
|
||||
const onConnectionAttached = (data: any) => {
|
||||
dispatch(connectionAttached(data));
|
||||
}
|
||||
|
||||
const onGraphUpdate = (graphData: any) => {
|
||||
debouncedOnGraphUpdate(graphData);
|
||||
};
|
||||
|
||||
const onCodeUpdate = (code: string) => {
|
||||
debouncedOnCodeChange(code);
|
||||
};
|
||||
|
||||
const onCanvasMousewheel = (e: any) => {
|
||||
if (e.deltaY < 0) {
|
||||
_setScale(_scale + _scale * 0.25);
|
||||
}
|
||||
|
||||
if (e.deltaY > 0) {
|
||||
_setScale(_scale - _scale * 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
const onCanvasMouseUpLeave = (e: any) => {
|
||||
if (dragging) {
|
||||
let left = _left + e.pageX - _initX;
|
||||
let top = _top + e.pageY - _initY;
|
||||
|
||||
_setLeft(left);
|
||||
_setTop(top);
|
||||
setDragging(false);
|
||||
dispatch(position({
|
||||
left: left,
|
||||
top: top
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const onCanvasMouseMove = (e: any) => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
"left": _left + e.pageX - _initX + 'px',
|
||||
"top": _top + e.pageY - _initY + 'px'
|
||||
}
|
||||
|
||||
setStyle(styles);
|
||||
}
|
||||
|
||||
const onCanvasMouseDown = (e: any) => {
|
||||
_setInitX(e.pageX);
|
||||
_setInitY(e.pageY);
|
||||
setDragging(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!generatedCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (language === "json") {
|
||||
setFormattedCode(JSON.stringify(YAML.parse(generatedCode), null, 2));
|
||||
}
|
||||
|
||||
if (language === "yaml") {
|
||||
setFormattedCode(generatedCode);
|
||||
}
|
||||
}, [language, generatedCode]);
|
||||
|
||||
useEffect(() => {
|
||||
setInstanceNodes(state.nodes);
|
||||
}, [state.nodes]);
|
||||
|
||||
useEffect(() => {
|
||||
setInstanceConnections(state.connections);
|
||||
}, [state.connections]);
|
||||
|
||||
useEffect(() => {
|
||||
setZoom(scale);
|
||||
dispatch(position({
|
||||
scale: scale
|
||||
}));
|
||||
}, [dispatch, scale, setZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
const styles = {
|
||||
"left": _left + 'px',
|
||||
"top": _top + 'px'
|
||||
}
|
||||
|
||||
setStyle(styles);
|
||||
}, [_left, _top, setStyle]);
|
||||
|
||||
useEffect(() => {
|
||||
_setTop(state.canvasPosition.top);
|
||||
_setLeft(state.canvasPosition.left);
|
||||
_setScale(state.canvasPosition.scale);
|
||||
}, [state.canvasPosition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModalCreateStep
|
||||
? <ModalServiceCreate
|
||||
onHide={() => setShowModalCreateStep(false)}
|
||||
onAddEndpoint={(values: any) => onAddEndpoint(values)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
{showModalEditStep
|
||||
? <ModalServiceEdit
|
||||
node={selectedNode}
|
||||
onHide={() => setShowModalEditStep(false)}
|
||||
onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
{showModalCreate
|
||||
? <ModalCreate
|
||||
onHide={() => setShowModalCreate(false)}
|
||||
onAddEndpoint={(values: any) => onAddEndpoint(values)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
{showModalEdit
|
||||
? <ModalEdit
|
||||
node={selectedNode}
|
||||
onHide={() => setShowModalEdit(false)}
|
||||
onUpdateEndpoint={(nodeItem: IClientNodeItem) => onUpdateEndpoint(nodeItem)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
|
||||
{instanceNodes &&
|
||||
<>
|
||||
<div className="w-full overflow-hidden md:w-2/3 z-40" style={{height: height}}>
|
||||
<div className="relative h-full">
|
||||
<div className="absolute top-0 right-0 z-40">
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="hidden btn-util" type="button" onClick={zoomOut} disabled={scale <= 0.5}>-</button>
|
||||
<button className="hidden btn-util" type="button" onClick={zoomIn} disabled={scale >= 1}>+</button>
|
||||
<button className="flex space-x-1 btn-util" type="button" onClick={() => setShowModalCreateStep(true)}>
|
||||
<PlusIcon className="w-3"/>
|
||||
<span>Service</span>
|
||||
</button>
|
||||
<button className="btn-util" type="button" onClick={() => setShowModalCreate(true)}>
|
||||
Volumes
|
||||
</button>
|
||||
<button className="btn-util" type="button" onClick={() => setShowModalCreate(true)}>
|
||||
Networks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div key={CANVAS_ID} className="jsplumb-box"
|
||||
onWheel={onCanvasMousewheel}
|
||||
onMouseMove={onCanvasMouseMove}
|
||||
onMouseDown={onCanvasMouseDown}
|
||||
onMouseUp={onCanvasMouseUpLeave}
|
||||
onMouseLeave={onCanvasMouseUpLeave}
|
||||
onContextMenu={(event) => { event.stopPropagation(); event.preventDefault(); }}
|
||||
>
|
||||
<div
|
||||
id={CANVAS_ID}
|
||||
ref={containerCallbackRef}
|
||||
className="canvas h-full w-full"
|
||||
style={{
|
||||
transformOrigin: '0px 0px 0px',
|
||||
transform: `translate(${translateWidth}px, ${translateHeight}px) scale(${scale})`
|
||||
}}
|
||||
>
|
||||
{(values(instanceNodes).length > 0) && (
|
||||
<>
|
||||
{values(instanceNodes).map((x) => (
|
||||
<div
|
||||
key={x.key}
|
||||
className={"node-item cursor-pointer shadow flex flex-col"}
|
||||
id={x.key}
|
||||
style={{ top: x.position.top, left: x.position.left }}
|
||||
>
|
||||
<div className="node-label w-full py-2 px-4">
|
||||
<div className="text-sm font-semibold">
|
||||
{x.configuration.prettyName}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{x.configuration.prettyName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group code-column w-full md:w-1/3">
|
||||
<div className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}>
|
||||
<button className={`btn-util ${language === "json" ? `btn-util-selected` : ``}`} onClick={() => setLanguage('json')}>json</button>
|
||||
<button className={`btn-util ${language === "yaml" ? `btn-util-selected` : ``}`} onClick={() => setLanguage('yaml')}>yaml</button>
|
||||
<button className="btn-util" type="button" onClick={copy}>{copyText}</button>
|
||||
</div>
|
||||
|
||||
<CodeEditor
|
||||
data={formattedCode}
|
||||
language={language}
|
||||
onChange={(e: any) => {onCodeUpdate(e)}}
|
||||
disabled={false}
|
||||
lineWrapping={false}
|
||||
height={height}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,122 @@
|
||||
import { StreamLanguage } from "@codemirror/stream-parser";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import {
|
||||
EditorView,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
} from '@codemirror/view'
|
||||
import { jsonLanguage } from "@codemirror/lang-json";
|
||||
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
|
||||
|
||||
import { history, historyKeymap } from '@codemirror/history'
|
||||
import { foldGutter, foldKeymap } from '@codemirror/fold'
|
||||
import { bracketMatching } from '@codemirror/matchbrackets'
|
||||
import { closeBrackets, closeBracketsKeymap } from '@codemirror/closebrackets'
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'
|
||||
import { autocompletion, completionKeymap } from '@codemirror/autocomplete'
|
||||
import { rectangularSelection } from '@codemirror/rectangular-selection'
|
||||
import { commentKeymap } from '@codemirror/comment'
|
||||
import { lintKeymap } from '@codemirror/lint'
|
||||
import { indentOnInput, LanguageSupport } from '@codemirror/language'
|
||||
import { lineNumbers } from '@codemirror/gutter';
|
||||
import { defaultKeymap, indentMore, indentLess } from '@codemirror/commands'
|
||||
import { defaultHighlightStyle } from '@codemirror/highlight'
|
||||
import { solarizedDark } from './themes/ui/dark'
|
||||
import darkHighlightStyle from './themes/highlight/dark'
|
||||
import useCodeMirror from "./useCodeMirror";
|
||||
|
||||
|
||||
interface ICodeEditorProps {
|
||||
data: string;
|
||||
language: string;
|
||||
onChange: any;
|
||||
disabled: boolean;
|
||||
lineWrapping: boolean;
|
||||
height: number
|
||||
}
|
||||
|
||||
const languageExtensions: any = {
|
||||
json: [new LanguageSupport(jsonLanguage)],
|
||||
yaml: [StreamLanguage.define(yaml)],
|
||||
blank: undefined
|
||||
}
|
||||
|
||||
const themeExtensions = {
|
||||
light: [defaultHighlightStyle],
|
||||
dark: [solarizedDark]
|
||||
}
|
||||
|
||||
const highlightExtensions = {
|
||||
dark: darkHighlightStyle
|
||||
}
|
||||
|
||||
const CodeEditor = (props: ICodeEditorProps) => {
|
||||
const { data, language, onChange, disabled, lineWrapping, height } = props;
|
||||
const extensions = [[
|
||||
lineNumbers(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
...(languageExtensions[language]
|
||||
? languageExtensions[language]
|
||||
: []),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...commentKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap,
|
||||
{
|
||||
key: "Tab",
|
||||
preventDefault: true,
|
||||
run: indentMore,
|
||||
},
|
||||
{
|
||||
key: "Shift-Tab",
|
||||
preventDefault: true,
|
||||
run: indentLess,
|
||||
},
|
||||
/*{
|
||||
key: "Ctrl-S",
|
||||
preventDefault: true,
|
||||
run: indentLess,
|
||||
}*/
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.changes) {
|
||||
onChange(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
...(disabled
|
||||
? [EditorState.readOnly.of(true)]
|
||||
: [EditorState.readOnly.of(false)]),
|
||||
...(lineWrapping
|
||||
? [EditorView.lineWrapping]
|
||||
: []),
|
||||
...[themeExtensions["dark"]],
|
||||
...[highlightExtensions["dark"]]
|
||||
]];
|
||||
|
||||
const { ref } = useCodeMirror(extensions, data);
|
||||
|
||||
return (
|
||||
<div className={`overflow-y-auto py-2`} style={{ height: height }} ref={ref}>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeEditor;
|
||||
@ -0,0 +1,82 @@
|
||||
// Based on https://github.com/codemirror/theme-one-dark
|
||||
// Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
// MIT License: https://github.com/codemirror/theme-one-dark/blob/main/LICENSE
|
||||
|
||||
import { HighlightStyle, tags as t } from "@codemirror/highlight"
|
||||
|
||||
const chalky = "#e5c07b",
|
||||
coral = "#e06c75",
|
||||
cyan = "#56b6c2",
|
||||
invalid = "#ffffff",
|
||||
ivory = "#abb2bf",
|
||||
stone = "#5c6370",
|
||||
malibu = "#61afef",
|
||||
sage = "#98c379",
|
||||
whiskey = "#d19a66",
|
||||
violet = "#c678dd"
|
||||
|
||||
/// The highlighting style for code in the One Dark theme.
|
||||
export default HighlightStyle.define([
|
||||
{
|
||||
tag: t.keyword,
|
||||
color: violet
|
||||
},
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: coral
|
||||
},
|
||||
{
|
||||
tag: [t.processingInstruction, t.string, t.inserted],
|
||||
color: sage
|
||||
},
|
||||
{
|
||||
tag: [t.function(t.variableName), t.labelName],
|
||||
color: malibu
|
||||
},
|
||||
{
|
||||
tag: [t.color, t.constant(t.name), t.standard(t.name)],
|
||||
color: whiskey
|
||||
},
|
||||
{
|
||||
tag: [t.definition(t.name), t.separator],
|
||||
color: ivory
|
||||
},
|
||||
{
|
||||
tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
|
||||
color: chalky
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
|
||||
color: cyan
|
||||
},
|
||||
{
|
||||
tag: [t.meta, t.comment],
|
||||
color: stone
|
||||
},
|
||||
{
|
||||
tag: t.strong,
|
||||
fontWeight: "bold"
|
||||
},
|
||||
{
|
||||
tag: t.emphasis,
|
||||
fontStyle: "italic"
|
||||
},
|
||||
{
|
||||
tag: t.link,
|
||||
color: stone,
|
||||
textDecoration: "underline"
|
||||
},
|
||||
{
|
||||
tag: t.heading,
|
||||
fontWeight: "bold",
|
||||
color: coral
|
||||
},
|
||||
{
|
||||
tag: [t.atom, t.bool, t.special(t.variableName)],
|
||||
color: whiskey
|
||||
},
|
||||
{
|
||||
tag: t.invalid,
|
||||
color: invalid
|
||||
},
|
||||
]);
|
||||
@ -0,0 +1,172 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { HighlightStyle, tags } from '@codemirror/highlight';
|
||||
|
||||
const base00 = '#1F2937', base01 = '#073642', base02 = '#586e75', base03 = '#657b83', base04 = '#839496', base05 = '#dfdfdf', base06 = '#efefef', base07 = '#fdf6e3', base_red = '#dc322f', base_orange = '#cb4b16', base_yellow = '#e9b100', base_green = '#cfec11', base_cyan = '#44e0d4', base_blue = '#75c6ff', base_violet = '#a1a6ff', base_magenta = '#d33682';
|
||||
const invalid = '#d30102', stone = base04, darkBackground = '#1F2937', highlightBackground = '#173541', background = base00, tooltipBackground = base01, selection = '#173541', cursor = base04;
|
||||
/**
|
||||
The editor theme styles for Solarized Dark.
|
||||
*/
|
||||
const solarizedDarkTheme = /*@__PURE__*/EditorView.theme({
|
||||
'&': {
|
||||
fontSize: "10.5pt",
|
||||
color: base05,
|
||||
backgroundColor: background
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: cursor
|
||||
},
|
||||
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { backgroundColor: selection },
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: base03 },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff'
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f'
|
||||
},
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
outline: `1px solid ${base06}`
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: darkBackground,
|
||||
color: stone,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: highlightBackground
|
||||
},
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd'
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: 'none',
|
||||
backgroundColor: tooltipBackground
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: highlightBackground,
|
||||
color: base03
|
||||
}
|
||||
}
|
||||
}, { dark: true });
|
||||
/**
|
||||
The highlighting style for code in the Solarized Dark theme.
|
||||
*/
|
||||
const solarizedDarkHighlightStyle = /*@__PURE__*/HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: base_green },
|
||||
{
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
color: base_cyan
|
||||
},
|
||||
{ tag: [tags.variableName], color: base05 },
|
||||
{ tag: [/*@__PURE__*/tags.function(tags.variableName)], color: base_blue },
|
||||
{ tag: [tags.labelName], color: base_magenta },
|
||||
{
|
||||
tag: [tags.color, /*@__PURE__*/tags.constant(tags.name), /*@__PURE__*/tags.standard(tags.name)],
|
||||
color: base_yellow
|
||||
},
|
||||
{ tag: [/*@__PURE__*/tags.definition(tags.name), tags.separator], color: base_cyan },
|
||||
{ tag: [tags.brace], color: base_magenta },
|
||||
{
|
||||
tag: [tags.annotation],
|
||||
color: invalid
|
||||
},
|
||||
{
|
||||
tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace],
|
||||
color: base_magenta
|
||||
},
|
||||
{
|
||||
tag: [tags.typeName, tags.className],
|
||||
color: base_orange
|
||||
},
|
||||
{
|
||||
tag: [tags.operator, tags.operatorKeyword],
|
||||
color: base_violet
|
||||
},
|
||||
{
|
||||
tag: [tags.tagName],
|
||||
color: base_blue
|
||||
},
|
||||
{
|
||||
tag: [tags.squareBracket],
|
||||
color: base_red
|
||||
},
|
||||
{
|
||||
tag: [tags.angleBracket],
|
||||
color: base02
|
||||
},
|
||||
{
|
||||
tag: [tags.attributeName],
|
||||
color: base05
|
||||
},
|
||||
{
|
||||
tag: [tags.regexp],
|
||||
color: invalid
|
||||
},
|
||||
{
|
||||
tag: [tags.quote],
|
||||
color: base_green
|
||||
},
|
||||
{ tag: [tags.string], color: base_yellow },
|
||||
{
|
||||
tag: [tags.url, tags.escape, /*@__PURE__*/tags.special(tags.string)],
|
||||
color: base_yellow
|
||||
},
|
||||
{ tag: [tags.meta], color: base_red },
|
||||
{ tag: [tags.comment], color: base02, fontStyle: 'italic' },
|
||||
{ tag: tags.strong, fontWeight: 'bold', color: base06 },
|
||||
{ tag: tags.emphasis, fontStyle: 'italic', color: base_green },
|
||||
{ tag: tags.strikethrough, textDecoration: 'line-through' },
|
||||
{
|
||||
tag: tags.link,
|
||||
color: base_cyan,
|
||||
textDecoration: 'underline',
|
||||
textUnderlinePosition: 'under'
|
||||
},
|
||||
{ tag: tags.heading, fontWeight: 'bold', color: base_yellow },
|
||||
{ tag: tags.heading1, fontWeight: 'bold', color: base07 },
|
||||
{
|
||||
tag: [tags.heading2, tags.heading3, tags.heading4],
|
||||
fontWeight: 'bold',
|
||||
color: base06
|
||||
},
|
||||
{
|
||||
tag: [tags.heading5, tags.heading6],
|
||||
color: base06
|
||||
},
|
||||
{ tag: [tags.atom, tags.bool, /*@__PURE__*/tags.special(tags.variableName)], color: base_magenta },
|
||||
{
|
||||
tag: [tags.processingInstruction, tags.inserted, tags.contentSeparator],
|
||||
color: base_red
|
||||
},
|
||||
{
|
||||
tag: [tags.contentSeparator],
|
||||
color: base_yellow
|
||||
},
|
||||
{ tag: tags.invalid, color: base02, borderBottom: `1px dotted ${base_red}` }
|
||||
]);
|
||||
/**
|
||||
Extension to enable the Solarized Dark theme (both the editor theme and
|
||||
the highlight style).
|
||||
*/
|
||||
const solarizedDark = [
|
||||
solarizedDarkTheme,
|
||||
solarizedDarkHighlightStyle
|
||||
];
|
||||
|
||||
export { solarizedDark, solarizedDarkHighlightStyle, solarizedDarkTheme };
|
||||
@ -0,0 +1,30 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { Extension } from "@codemirror/state";
|
||||
|
||||
export default function useCodeMirror(extensions: Extension[], doc: any) {
|
||||
const [element, setElement] = useState<HTMLElement>();
|
||||
|
||||
const ref = useCallback((node: HTMLElement | null) => {
|
||||
if (!node) return;
|
||||
|
||||
setElement(node);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) return;
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: doc,
|
||||
extensions: [...extensions],
|
||||
}),
|
||||
parent: element,
|
||||
});
|
||||
|
||||
return () => view?.destroy();
|
||||
}, [element, extensions, doc]);
|
||||
|
||||
return { ref };
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { MoonIcon, SunIcon } from "@heroicons/react/outline";
|
||||
import { useDarkMode } from "./userDarkMode";
|
||||
|
||||
interface IDarkModeSwitchProps {
|
||||
}
|
||||
|
||||
const DarkModeSwitch = (props: IDarkModeSwitchProps) => {
|
||||
const [isDark, setIsDark] = useDarkMode();
|
||||
|
||||
return (
|
||||
<div className="flex flex items-center">
|
||||
<button
|
||||
onClick={e => setIsDark(!isDark)}
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
className="
|
||||
text-gray-500
|
||||
dark:text-gray-200
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-900
|
||||
focus:outline-none
|
||||
dark:focus:ring-gray-700
|
||||
rounded-lg
|
||||
text-sm
|
||||
p-2.5"
|
||||
>
|
||||
{isDark
|
||||
? <SunIcon id="theme-toggle-light-icon" className="w-5 h-5" />
|
||||
: <MoonIcon id="theme-toggle-dark-icon" className="w-5 h-5" />
|
||||
}
|
||||
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DarkModeSwitch;
|
||||
@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function usePrefersDarkMode() {
|
||||
const [value, setValue] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
setValue(mediaQuery.matches);
|
||||
|
||||
const handler = () => setValue(mediaQuery.matches);
|
||||
mediaQuery.addEventListener("change", handler);
|
||||
return () => mediaQuery.removeEventListener("change", handler);
|
||||
}, [])
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useSafeLocalStorage(key: string, initialValue: string | undefined) {
|
||||
const [valueProxy, setValueProxy] = useState(() => {
|
||||
try {
|
||||
const value = window.localStorage.getItem(key);
|
||||
return value ? JSON.parse(value) : initialValue;
|
||||
} catch {
|
||||
return initialValue;
|
||||
}
|
||||
})
|
||||
|
||||
const setValue = (value: string) => {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
setValueProxy(value);
|
||||
} catch {
|
||||
setValueProxy(value);
|
||||
}
|
||||
}
|
||||
|
||||
return [valueProxy, setValue];
|
||||
}
|
||||
|
||||
export function useDarkMode() {
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
const [isEnabled, setIsEnabled] = useSafeLocalStorage("dark-mode", undefined);
|
||||
const enabled = isEnabled === undefined ? prefersDarkMode : isEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
if (window === undefined) return;
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove(enabled ? "light" : "dark");
|
||||
root.classList.add(enabled ? "dark" : "light");
|
||||
}, [enabled]);
|
||||
|
||||
return [enabled, setIsEnabled];
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
const Container = (props: any) => {
|
||||
const { formik } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor={`container-name`} className="block text-xs font-medium text-gray-700">Name</label>
|
||||
<div key={`container-name`}>
|
||||
<input
|
||||
type="text"
|
||||
className="input-util"
|
||||
name={`configuration.container.name`}
|
||||
value={formik.values.configuration.container.name || ""}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor={`container-image`} className="block text-xs font-medium text-gray-700 mt-2">Image</label>
|
||||
<div key={`container-image`}>
|
||||
<input
|
||||
type="text"
|
||||
className="input-util"
|
||||
name={`configuration.container.image`}
|
||||
value={formik.values.configuration.container.image || ""}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor={`container-pull-policy`} className="block text-xs font-medium text-gray-700 mt-2">Pull policy</label>
|
||||
<div key={`container-pull-policy`}>
|
||||
<input
|
||||
type="text"
|
||||
className="input-util"
|
||||
name={`configuration.container.imagePullPolicy`}
|
||||
value={formik.values.configuration.container.imagePullPolicy || ""}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Container;
|
||||
@ -0,0 +1,147 @@
|
||||
import { useState } from "react";
|
||||
import { useFormik } from "formik";
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import General from "./General";
|
||||
import Container from "./Container";
|
||||
import Resource from "./Resource";
|
||||
import { initialValues, formatName } from "./../../../utils";
|
||||
|
||||
|
||||
interface IModalProps {
|
||||
onHide: any;
|
||||
onAddEndpoint: Function;
|
||||
}
|
||||
|
||||
const ModalCreate = (props: IModalProps) => {
|
||||
const { onHide, onAddEndpoint } = props;
|
||||
const [openTab, setOpenTab] = useState("General");
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
configuration: {
|
||||
...initialValues(),
|
||||
},
|
||||
key: "template",
|
||||
type: "TEMPLATE",
|
||||
inputs: ["op_source"],
|
||||
outputs: [],
|
||||
config: {}
|
||||
},
|
||||
onSubmit: ((values, { setSubmitting }) => {
|
||||
|
||||
})
|
||||
});
|
||||
const tabs = [
|
||||
{ name: 'General', href: '#', current: true, hidden: false },
|
||||
{ name: 'Container', href: '#', current: false, hidden: (formik.values.configuration.type === 'container' ? false : true) },
|
||||
{ name: 'Resource', href: '#', current: false, hidden: (formik.values.configuration.type === 'resource' ? false : true) }
|
||||
];
|
||||
|
||||
const classNames = (...classes: string[]) => {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
||||
<div onClick={onHide} className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
||||
<h3 className="text-sm font-semibold">Add template</h3>
|
||||
<button
|
||||
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
||||
onClick={onHide}
|
||||
>
|
||||
<span className="block outline-none focus:outline-none">
|
||||
<XIcon className="w-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="sm:hidden">
|
||||
<label htmlFor="tabs" className="sr-only">
|
||||
Select a tab
|
||||
</label>
|
||||
<select
|
||||
id="tabs"
|
||||
name="tabs"
|
||||
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||
defaultValue={tabs.find((tab) => tab.current)!.name}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name}>{tab.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<div className="border-b border-gray-200 px-8">
|
||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab, index) => (
|
||||
<a
|
||||
key={tab.name}
|
||||
href={tab.href}
|
||||
className={classNames(
|
||||
tab.name === openTab
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||
'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm',
|
||||
(tab.hidden)
|
||||
? 'hidden'
|
||||
: ''
|
||||
)}
|
||||
aria-current={tab.current ? 'page' : undefined}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setOpenTab(tab.name);
|
||||
}}
|
||||
>
|
||||
{tab.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative px-4 py-3 flex-auto">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
{openTab === "General" &&
|
||||
<General formik={formik} />
|
||||
}
|
||||
|
||||
{openTab === "Container" &&
|
||||
<Container formik={formik} />
|
||||
}
|
||||
|
||||
{openTab === "Resource" &&
|
||||
<Resource formik={formik} />
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
||||
<button
|
||||
className="btn-util"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
formik.values.configuration.name = formatName(formik.values.configuration.prettyName);
|
||||
onAddEndpoint(formik.values);
|
||||
formik.resetForm();
|
||||
setOpenTab("General");
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalCreate
|
||||
@ -0,0 +1,165 @@
|
||||
import React from "react";
|
||||
import { useFormik } from "formik";
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import General from "./General";
|
||||
import Container from "./Container";
|
||||
import Resource from "./Resource";
|
||||
import { initialValues, formatName } from "./../../../utils";
|
||||
import { IClientNodeItem } from "../../../types";
|
||||
|
||||
|
||||
interface IModalProps {
|
||||
node: IClientNodeItem | null;
|
||||
onHide: any;
|
||||
onUpdateEndpoint: any;
|
||||
}
|
||||
|
||||
const ModalEdit = (props: IModalProps) => {
|
||||
const { node, onHide, onUpdateEndpoint } = props;
|
||||
const [selectedNode, setSelectedNode] = React.useState<IClientNodeItem | null>(null);
|
||||
const [openTab, setOpenTab] = React.useState("General");
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
configuration: {
|
||||
...initialValues()
|
||||
}
|
||||
},
|
||||
onSubmit: ((values, { setSubmitting }) => {
|
||||
|
||||
})
|
||||
});
|
||||
const tabs = [
|
||||
{ name: 'General', href: '#', current: true, hidden: false },
|
||||
{ name: 'Container', href: '#', current: false, hidden: (formik.values.configuration.type === 'container' ? false : true) },
|
||||
{ name: 'Resource', href: '#', current: false, hidden: (formik.values.configuration.type === 'resource' ? false : true) }
|
||||
];
|
||||
|
||||
const classNames = (...classes: string[]) => {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (node) {
|
||||
setSelectedNode(node);
|
||||
}
|
||||
}, [node]);
|
||||
|
||||
React.useEffect(() => {
|
||||
formik.resetForm();
|
||||
|
||||
if (selectedNode) {
|
||||
formik.initialValues.configuration = { ...selectedNode.configuration };
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
formik.resetForm();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
||||
<div onClick={onHide} className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
||||
<h3 className="text-sm font-semibold">Update template</h3>
|
||||
<button
|
||||
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
||||
onClick={onHide}
|
||||
>
|
||||
<span className="block outline-none focus:outline-none">
|
||||
<XIcon className="w-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="sm:hidden">
|
||||
<label htmlFor="tabs" className="sr-only">
|
||||
Select a tab
|
||||
</label>
|
||||
<select
|
||||
id="tabs"
|
||||
name="tabs"
|
||||
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||
defaultValue={tabs.find((tab) => tab.current)!.name}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name}>{tab.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<div className="border-b border-gray-200 px-8">
|
||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab, index) => (
|
||||
<a
|
||||
key={tab.name}
|
||||
href={tab.href}
|
||||
className={classNames(
|
||||
tab.name === openTab
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
|
||||
'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm',
|
||||
(tab.hidden)
|
||||
? 'hidden'
|
||||
: ''
|
||||
)}
|
||||
aria-current={tab.current ? 'page' : undefined}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setOpenTab(tab.name);
|
||||
}}
|
||||
>
|
||||
{tab.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative px-4 py-3 flex-auto">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
{openTab === "General" &&
|
||||
<General formik={formik} />
|
||||
}
|
||||
|
||||
{openTab === "Container" &&
|
||||
<Container formik={formik} />
|
||||
}
|
||||
|
||||
{openTab === "Resource" &&
|
||||
<Resource formik={formik} />
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
||||
<button
|
||||
className="btn-util"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let updated = { ...selectedNode };
|
||||
formik.values.configuration.name = formatName(formik.values.configuration.prettyName);
|
||||
updated.configuration = formik.values.configuration;
|
||||
onUpdateEndpoint(updated);
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalEdit
|
||||
@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
|
||||
const General = (props: any) => {
|
||||
const { formik } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="prettyName" className="block text-xs font-medium text-gray-700">Name</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="prettyName"
|
||||
name="configuration.prettyName"
|
||||
type="text"
|
||||
autoComplete="none"
|
||||
className="input-util"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.prettyName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<label htmlFor="about" className="block text-xs font-medium text-gray-700">Description</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
id="description"
|
||||
name="configuration.description"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.description}
|
||||
rows={2}
|
||||
className="input-util"
|
||||
placeholder=""
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mt-2">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="templateType" className="block text-xs font-medium text-gray-700">Type</label>
|
||||
<div className="mt-1">
|
||||
<select
|
||||
id="templateType"
|
||||
name="configuration.type"
|
||||
className="max-w-lg block focus:ring-indigo-500 focus:border-indigo-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
|
||||
value={formik.values.configuration.type}
|
||||
onChange={formik.handleChange}
|
||||
>
|
||||
<option value="">Select type</option>
|
||||
<option value="container">Container</option>
|
||||
<option value="resource">Resource</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default General;
|
||||
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
|
||||
const Resource = (props: any) => {
|
||||
const { formik } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor={`resource-action`} className="block text-xs font-medium text-gray-700">Action</label>
|
||||
<div key={`resource-action`}>
|
||||
<input
|
||||
type="text"
|
||||
className="input-util"
|
||||
name={`configuration.resource.action`}
|
||||
value={formik.values.configuration.resource.action || ""}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor={`resource-manifest`} className="block text-xs font-medium text-gray-700 mt-2">Manifest</label>
|
||||
<textarea
|
||||
id="resource-manifest"
|
||||
rows={2}
|
||||
className="input-util"
|
||||
placeholder=""
|
||||
name={`configuration.resource.manifest`}
|
||||
value={formik.values.configuration.resource.manifest || ""}
|
||||
onChange={formik.handleChange}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Resource;
|
||||
@ -0,0 +1,105 @@
|
||||
import { useFormik } from "formik";
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import { serviceInitialValues, formatName } from "../../../utils";
|
||||
|
||||
|
||||
interface IModalServiceProps {
|
||||
onHide: any;
|
||||
onAddEndpoint: Function;
|
||||
}
|
||||
|
||||
const ModalServiceCreate = (props: IModalServiceProps) => {
|
||||
const { onHide, onAddEndpoint } = props;
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
configuration: {
|
||||
...serviceInitialValues(),
|
||||
},
|
||||
key: "service",
|
||||
type: "SERVICE",
|
||||
inputs: ["op_source"],
|
||||
outputs: [],
|
||||
config: {}
|
||||
},
|
||||
onSubmit: ((values, { setSubmitting }) => {
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
||||
<div onClick={onHide} className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
||||
<h3 className="text-sm font-semibold">Add service</h3>
|
||||
<button
|
||||
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
||||
onClick={onHide}
|
||||
>
|
||||
<span className="block outline-none focus:outline-none">
|
||||
<XIcon className="w-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative px-4 py-3 flex-auto">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="prettyName" className="block text-xs font-medium text-gray-700">Name</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="prettyName"
|
||||
name="configuration.prettyName"
|
||||
type="text"
|
||||
autoComplete="none"
|
||||
className="input-util"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.prettyName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="template" className="block text-xs font-medium text-gray-700">Template</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="template"
|
||||
name="configuration.template"
|
||||
type="text"
|
||||
autoComplete="none"
|
||||
className="input-util"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.template}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
||||
<button
|
||||
className="btn-util"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
formik.values.configuration.name = formatName(formik.values.configuration.prettyName);
|
||||
onAddEndpoint(formik.values);
|
||||
formik.resetForm();
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalServiceCreate
|
||||
@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
import { useFormik } from "formik";
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import { serviceInitialValues, formatName } from "../../../utils";
|
||||
|
||||
|
||||
interface IModalServiceProps {
|
||||
node: any;
|
||||
onHide: any;
|
||||
onUpdateEndpoint: any;
|
||||
}
|
||||
|
||||
const ModalServiceEdit = (props: IModalServiceProps) => {
|
||||
const { node, onHide, onUpdateEndpoint } = props;
|
||||
const [selectedNode, setSelectedNode] = React.useState<any>(null);
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
configuration: {
|
||||
...serviceInitialValues()
|
||||
}
|
||||
},
|
||||
onSubmit: ((values, { setSubmitting }) => {
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (node) {
|
||||
setSelectedNode(node);
|
||||
}
|
||||
}, [node]);
|
||||
|
||||
React.useEffect(() => {
|
||||
formik.resetForm();
|
||||
|
||||
if (selectedNode) {
|
||||
formik.initialValues.configuration = { ...selectedNode.configuration };
|
||||
}
|
||||
}, [selectedNode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
formik.resetForm();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
||||
<div onClick={onHide} className="opacity-25 fixed inset-0 z-40 bg-black"></div>
|
||||
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
||||
<h3 className="text-sm font-semibold">Update service</h3>
|
||||
<button
|
||||
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
||||
onClick={onHide}
|
||||
>
|
||||
<span className="block outline-none focus:outline-none">
|
||||
<XIcon className="w-4" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative px-4 py-3 flex-auto">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="prettyName" className="block text-xs font-medium text-gray-700">Name</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="prettyName"
|
||||
name="configuration.prettyName"
|
||||
type="text"
|
||||
autoComplete="none"
|
||||
className="input-util"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.prettyName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="col-span-3">
|
||||
<label htmlFor="template" className="block text-xs font-medium text-gray-700">Template</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="template"
|
||||
name="configuration.template"
|
||||
type="text"
|
||||
autoComplete="none"
|
||||
className="input-util"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.configuration.template}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
||||
<button
|
||||
className="btn-util"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let updated = { ...selectedNode };
|
||||
formik.values.configuration.name = formatName(formik.values.configuration.prettyName);
|
||||
updated.configuration = formik.values.configuration;
|
||||
onUpdateEndpoint(updated);
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalServiceEdit
|
||||
@ -0,0 +1,94 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MenuAlt2Icon, ReplyIcon } from "@heroicons/react/outline";
|
||||
import SideBar from "../../components/SideBar";
|
||||
import DarkModeSwitch from "../../components/DarkModeSwitch";
|
||||
import { LOCAL_STORAGE } from "../../constants";
|
||||
import { authSelf } from "../../reducers";
|
||||
|
||||
interface IProfileProps {
|
||||
dispatch: any;
|
||||
state: any;
|
||||
}
|
||||
|
||||
const Profile = (props: IProfileProps) => {
|
||||
const { dispatch, state } = props;
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const logOut = () => {
|
||||
localStorage.removeItem(LOCAL_STORAGE);
|
||||
dispatch(authSelf(null));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBar state={state} sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
|
||||
|
||||
<div className="md:pl-56 flex flex-col flex-1">
|
||||
<div className="dark:bg-gray-800 sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 border-r dark:border-gray-900 border-gray-200 text-gray-500 md:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<MenuAlt2Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="flex-1 px-4 sm:px-6 md:px-8 flex justify-between items-center">
|
||||
<Link
|
||||
className="text-gray-700 dark:text-white"
|
||||
to="/"
|
||||
>
|
||||
<ReplyIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<div className="ml-4 flex md:ml-6">
|
||||
<DarkModeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="py-6">
|
||||
<div className="flex justify-between px-4 sm:px-6 md:px-8">
|
||||
<h1 className="text-2xl font-semibold dark:text-white text-gray-900">Profile</h1>
|
||||
<button
|
||||
className="flex items-center bg-blue-600 hover:bg-blue-700 text-white font-medium py-1 px-3 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={logOut}
|
||||
>
|
||||
<span className="text-sm">Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-4 gap-y-8 px-4 py-4 sm:px-6 md:flex-row md:px-8">
|
||||
{state.user &&
|
||||
<>
|
||||
{state.user.username &&
|
||||
<div className="sm:col-span-1">
|
||||
<dt className="text-sm font-medium dark:text-gray-200 text-gray-500">
|
||||
username
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm dark:text-white text-gray-900">
|
||||
{state.user.username}
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
|
||||
{state.user.email &&
|
||||
<div className="sm:col-span-1">
|
||||
<dt className="text-sm font-medium dark:text-gray-200 text-gray-500">
|
||||
email
|
||||
</dt>
|
||||
<dd className="mt-1 text-sm dark:text-white text-gray-900">
|
||||
{state.user.email}
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
@ -0,0 +1,203 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { IProjectPayload } from "../../types";
|
||||
import { nodes, connections, position, updateProjectName } from "../../reducers";
|
||||
import Spinner from "../Spinner";
|
||||
import { Canvas } from "../Canvas";
|
||||
import useWindowDimensions from "../../hooks/useWindowDimensions";
|
||||
import { getClientNodesAndConnections } from "../../utils";
|
||||
import { projectHttpGet, projectHttpUpdate, projectHttpCreate } from "../../services/project";
|
||||
import { checkHttpStatus } from "../../services/helpers";
|
||||
import { nodeLibraries } from "../../utils/data/libraries";
|
||||
|
||||
interface IProjectProps {
|
||||
dispatch: any;
|
||||
state: any;
|
||||
}
|
||||
|
||||
export default function Project(props: IProjectProps) {
|
||||
const { uuid } = useParams<{ uuid?: string }>();
|
||||
const { dispatch, state } = props;
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [projectName, setProjectName] = useState("");
|
||||
const { height, width } = useWindowDimensions();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNameChange = (e: any) => {
|
||||
setProjectName(e.target.value);
|
||||
dispatch(updateProjectName(e.target.value));
|
||||
}
|
||||
|
||||
const updateProject = (uuid: string, payload: IProjectPayload) => {
|
||||
projectHttpUpdate(uuid, JSON.stringify(payload))
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
})
|
||||
.finally(() => {
|
||||
setSaving(false);
|
||||
});
|
||||
}
|
||||
|
||||
const createProject = (payload: IProjectPayload) => {
|
||||
projectHttpCreate(JSON.stringify(payload))
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
navigate(`/projects/${data.uuid}`)
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
})
|
||||
.finally(() => {
|
||||
setSaving(false);
|
||||
});
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
setSaving(true);
|
||||
const payload: IProjectPayload = {
|
||||
name: state.projectName,
|
||||
data: {
|
||||
canvas: {
|
||||
position: state.canvasPosition,
|
||||
nodes: state.nodes,
|
||||
connections: state.connections
|
||||
},
|
||||
configs: [],
|
||||
networks: [],
|
||||
secrets: [],
|
||||
services: state.nodes,
|
||||
version: 3,
|
||||
volumes: [],
|
||||
}
|
||||
};
|
||||
|
||||
if (uuid) {
|
||||
updateProject(uuid, payload);
|
||||
} else {
|
||||
createProject(payload);
|
||||
}
|
||||
};
|
||||
|
||||
const setViewHeight = () => {
|
||||
let vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (uuid) {
|
||||
projectHttpGet(uuid)
|
||||
.then(checkHttpStatus)
|
||||
.then(data => {
|
||||
const projectData = JSON.parse(data.data);
|
||||
const nodesAsList = Object.keys(projectData.canvas.nodes).map((k) => {
|
||||
return projectData.canvas.nodes[k];
|
||||
});
|
||||
|
||||
const clientNodeItems = getClientNodesAndConnections(nodesAsList, nodeLibraries);
|
||||
setProjectName(data.name);
|
||||
dispatch(updateProjectName(data.name));
|
||||
dispatch(nodes(clientNodeItems));
|
||||
dispatch(connections(projectData.canvas.connections));
|
||||
dispatch(position(projectData.canvas.position));
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status === 404) {
|
||||
window.location.replace("/");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
//setFetching(false);
|
||||
})
|
||||
}
|
||||
}, [uuid, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
//setProjectName(state.projectName);
|
||||
}, [state.projectName]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", () => {
|
||||
setViewHeight();
|
||||
});
|
||||
|
||||
setViewHeight();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 py-3 border-b border-gray-200">
|
||||
<form className="flex flex-col space-y-2 md:flex-row md:justify-between items-center" autoComplete="off">
|
||||
<input
|
||||
className={`
|
||||
bg-gray-100
|
||||
appearance-none
|
||||
w-full
|
||||
md:w-1/2
|
||||
lg:w-1/3
|
||||
block
|
||||
text-gray-700
|
||||
border
|
||||
border-gray-100
|
||||
dark:bg-gray-900
|
||||
dark:text-white
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
py-2
|
||||
px-3
|
||||
leading-tight
|
||||
focus:outline-none
|
||||
focus:border-indigo-400
|
||||
focus:ring-0
|
||||
`}
|
||||
type="text"
|
||||
placeholder="Untitled"
|
||||
autoComplete="off"
|
||||
id="name"
|
||||
name="name"
|
||||
onChange={handleNameChange}
|
||||
value={projectName}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col space-y-2 w-full justify-end mb-4 md:flex-row md:space-y-0 md:space-x-2 md:mb-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
window.location.replace("/");
|
||||
}}
|
||||
type="button"
|
||||
className="btn-util text-black bg-gray-200 hover:bg-gray-300 sm:w-auto"
|
||||
>
|
||||
<div className="flex justify-center items-center space-x-2 mx-auto">
|
||||
<span>New</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onSave()}
|
||||
type="button"
|
||||
className="btn-util text-white bg-green-600 hover:bg-green-700 sm:w-auto"
|
||||
>
|
||||
<div className="flex justify-center items-center space-x-2 mx-auto">
|
||||
{saving &&
|
||||
<Spinner className="w-4 h-4 text-green-300" />
|
||||
}
|
||||
<span>Save</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-grow relative flex-col md:flex-row">
|
||||
<Canvas
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
height={(height - 64)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import React, { SyntheticEvent } from "react";
|
||||
import { TrashIcon } from "@heroicons/react/solid";
|
||||
|
||||
|
||||
export interface ICloseProps {
|
||||
id: string;
|
||||
onClose?: (id: string, source?: string, target?: string) => any;
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
const Close = (props: ICloseProps) => {
|
||||
const { id, onClose, source, target } = props;
|
||||
const handleClose = (event: SyntheticEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
if (onClose) {
|
||||
onClose(id, source, target);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='absolute -top-4 left-0' onClick={handleClose} title={id || 'UNKNOWN'}>
|
||||
<TrashIcon className="w-3.5 text-gray-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Close
|
||||
@ -0,0 +1,132 @@
|
||||
import { Fragment } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { DatabaseIcon, TemplateIcon, XIcon } from "@heroicons/react/outline";
|
||||
import UserMenu from "./UserMenu";
|
||||
import Logo from "./logo";
|
||||
|
||||
interface ISideBarProps {
|
||||
state: any;
|
||||
sidebarOpen: boolean;
|
||||
setSidebarOpen: any;
|
||||
}
|
||||
|
||||
export default function SideBar(props: ISideBarProps) {
|
||||
const { pathname } = useLocation();
|
||||
const { state, sidebarOpen, setSidebarOpen } = props;
|
||||
const navigation = [
|
||||
{ name: "Templates", href: "/", icon: TemplateIcon, current: ((pathname === "/" || pathname.includes("templates")) ? true : false) },
|
||||
{ name: "Connectors", href: "/connectors", icon: DatabaseIcon, current: (pathname.includes("connectors") ? true : false) }
|
||||
];
|
||||
const classNames = (...classes: any[]) => {
|
||||
return classes.filter(Boolean).join(" ")
|
||||
};
|
||||
const userName = state.user ? state.user.username : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
||||
<Dialog as="div" className="fixed inset-0 flex z-40 md:hidden" onClose={setSidebarOpen}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-700 bg-opacity-50" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<div className="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-blue-700">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute top-0 right-0 -mr-7 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex items-center justify-center h-5 w-5 rounded-full"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
|
||||
<div className="flex-shrink-0 flex items-center px-4">
|
||||
<Logo className="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex-1 h-0 overflow-y-auto">
|
||||
<nav className="px-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
|
||||
)}
|
||||
>
|
||||
<item.icon className="mr-4 flex-shrink-0 h-6 w-6" aria-hidden="true" />
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<UserMenu username={userName} current={pathname.includes("profile")} />
|
||||
</div>
|
||||
</Transition.Child>
|
||||
|
||||
<div className="flex-shrink-0 w-14" aria-hidden="true">
|
||||
{/* Dummy element to force sidebar to shrink to fit close icon */}
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<div className="hidden md:flex md:w-56 md:flex-col md:fixed md:inset-y-0">
|
||||
<div className="flex flex-col flex-grow pt-5 bg-blue-700 overflow-y-auto">
|
||||
<div className="flex items-center flex-shrink-0 px-4">
|
||||
<Logo className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="mt-5 flex-1 flex flex-col">
|
||||
<nav className="flex-1 px-2 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600",
|
||||
"group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||
)}
|
||||
>
|
||||
<item.icon className="mr-3 flex-shrink-0 h-5 w-5" aria-hidden="true" />
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<UserMenu username={userName} current={pathname.includes("profile")} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
interface ISpinnerProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
const Spinner = (props: ISpinnerProps) => {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg className={`animate-spin h-5 w-5 inline-block ${className}`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Spinner
|
||||
@ -0,0 +1,34 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UserCircleIcon } from "@heroicons/react/solid";
|
||||
|
||||
interface IUserMenuProps {
|
||||
username: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export default function UserMenu(props: IUserMenuProps) {
|
||||
const { username, current } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate("/profile");
|
||||
}}
|
||||
className={`
|
||||
${current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600"},
|
||||
flex border-t border-blue-800 p-4 w-full hover:cursor-pointer hover:bg-blue-600
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<UserCircleIcon className="inline-block h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-base font-medium text-white">{username}</p>
|
||||
<p className="text-sm font-medium text-indigo-200 group-hover:text-white">View profile</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
interface ILogoProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
const Logo = (props: ILogoProps) => {
|
||||
const { className } = props;
|
||||
return (
|
||||
<svg width="689" height="689" viewBox="0 0 689 689" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path opacity="0.8" d="M191.04 268.58H0.154419L0.154419 459.465H191.04V268.58Z" fill="white" />
|
||||
<path opacity="0.8" d="M191.04 497.306H0.154419L0.154419 688.192H191.04V497.306Z" fill="white" />
|
||||
<path opacity="0.8" d="M191.04 39.8536L0.154419 39.8536L0.154419 230.739H191.04V39.8536Z" fill="#4F95FF" />
|
||||
<path opacity="0.8" d="M419.766 268.58H228.881V459.465H419.766V268.58Z" fill="#4F95FF" />
|
||||
<path opacity="0.8" d="M419.766 497.306H228.881V688.192H419.766V497.306Z" fill="#4F95FF" />
|
||||
<path opacity="0.8" d="M648.493 497.306H457.607V688.192H648.493V497.306Z" fill="white" />
|
||||
<path opacity="0.8" d="M688.084 135.105L553.109 0.130157L418.134 135.105L553.109 270.081L688.084 135.105Z" fill="white" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
@ -0,0 +1,325 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { AnchorId } from "@jsplumb/common";
|
||||
import {
|
||||
BeforeDropParams,
|
||||
Connection,
|
||||
ConnectionDetachedParams,
|
||||
ConnectionEstablishedParams,
|
||||
ConnectionSelection,
|
||||
EVENT_CONNECTION,
|
||||
EVENT_CONNECTION_DETACHED,
|
||||
INTERCEPT_BEFORE_DROP
|
||||
} from "@jsplumb/core";
|
||||
import {
|
||||
BrowserJsPlumbInstance,
|
||||
newInstance,
|
||||
EVENT_DRAG_STOP,
|
||||
EVENT_CONNECTION_DBL_CLICK
|
||||
} from "@jsplumb/browser-ui";
|
||||
import {
|
||||
defaultOptions,
|
||||
inputAnchors,
|
||||
outputAnchors,
|
||||
sourceEndpoint,
|
||||
targetEndpoint
|
||||
} from "../utils/options";
|
||||
import { getConnections } from "../utils";
|
||||
import { IClientNodeItem } from "../types";
|
||||
import { Dictionary, isEqual } from "lodash";
|
||||
import { IAnchor } from "../types";
|
||||
|
||||
export const useJsPlumb = (
|
||||
nodes: Dictionary<IClientNodeItem>,
|
||||
connections: Array<[string, string]>,
|
||||
onGraphUpdate: Function,
|
||||
onEndpointPositionUpdate: Function,
|
||||
onConnectionAttached: Function,
|
||||
onConnectionDetached: Function
|
||||
): [(containerElement: HTMLDivElement) => void,
|
||||
(zoom: number) => void,
|
||||
(style: any) => void,
|
||||
(node: IClientNodeItem) => void] => {
|
||||
const [instance, setInstance] = useState<BrowserJsPlumbInstance>(null as any);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const stateRef = useRef<Dictionary<IClientNodeItem>>();
|
||||
stateRef.current = nodes;
|
||||
const containerCallbackRef = useCallback((containerElement: HTMLDivElement) => {
|
||||
containerRef.current = containerElement;
|
||||
}, []);
|
||||
|
||||
const addEndpoints = useCallback((
|
||||
el: Element,
|
||||
sourceAnchors: IAnchor[],
|
||||
targetAnchors: IAnchor[],
|
||||
maxConnections: number
|
||||
) => {
|
||||
sourceAnchors.forEach((x) => {
|
||||
let endpoint = sourceEndpoint;
|
||||
endpoint.maxConnections = maxConnections;
|
||||
|
||||
// arrow overlay for connector to specify
|
||||
// it's dependency on another service
|
||||
instance.addEndpoint(el, endpoint, {
|
||||
anchor: [[0.4, 0, 0, -1], [1, 0.4, 1, 0], [0.4, 1, 0, 1], [0, 0.4, -1, 0]],
|
||||
uuid: x.id,
|
||||
connectorOverlays: [{
|
||||
type: "PlainArrow",
|
||||
options: {
|
||||
width: 16,
|
||||
length: 16,
|
||||
location: 1,
|
||||
id: "arrow"
|
||||
},
|
||||
}]
|
||||
})
|
||||
});
|
||||
|
||||
targetAnchors.forEach((x) => {
|
||||
let endpoint = targetEndpoint;
|
||||
endpoint.maxConnections = maxConnections;
|
||||
|
||||
instance.addEndpoint(el, endpoint, {
|
||||
anchor: [[0.6, 0, 0, -1], [1, 0.6, 1, 0], [0.6, 1, 0, 1], [0, 0.6, -1, 0]],
|
||||
uuid: x.id
|
||||
});
|
||||
});
|
||||
}, [instance]);
|
||||
|
||||
const removeEndpoint = useCallback((node) => {
|
||||
const nodeConnections = instance.getConnections({ target: node.key });
|
||||
|
||||
if (nodeConnections) {
|
||||
Object.values(nodeConnections).forEach((conn) => {
|
||||
instance.deleteConnection(conn);
|
||||
});
|
||||
};
|
||||
|
||||
instance.removeAllEndpoints(document.getElementById(node.key) as Element);
|
||||
}, [instance]);
|
||||
|
||||
const getAnchors = (port: string[], anchorIds: AnchorId[]): IAnchor[] => {
|
||||
return port.map(
|
||||
(x, index): IAnchor => ({
|
||||
id: x,
|
||||
position: anchorIds[port.length === 1 ? 2 : index]
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getOverlayObject = (instance: BrowserJsPlumbInstance) => {
|
||||
return {
|
||||
type: "Label",
|
||||
options: {
|
||||
label: "x",
|
||||
location: .5,
|
||||
id: "remove-conn",
|
||||
cssClass: `
|
||||
block jtk-overlay remove-conn-btn text-xs leading-normal
|
||||
cursor-pointer text-white font-bold rounded-full w-5 h-5
|
||||
z-20 flex justify-center
|
||||
`,
|
||||
events: {
|
||||
click: (e: any) => {
|
||||
instance.deleteConnection(e.overlay.component as Connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setZoom = useCallback((zoom: number) => {
|
||||
if (instance) {
|
||||
instance.setZoom(zoom);
|
||||
}
|
||||
}, [instance]);
|
||||
|
||||
const setStyle = useCallback((style: any) => {
|
||||
let styles: { [key: string]: any } = {};
|
||||
const currentStyle = containerRef.current?.getAttribute("style");
|
||||
|
||||
if (currentStyle) {
|
||||
let currentStyleParts = (
|
||||
currentStyle
|
||||
.split(";")
|
||||
.map(element => element.trim())
|
||||
.filter(element => element !== '')
|
||||
);
|
||||
|
||||
for (let i = 0; i < currentStyleParts.length; i++) {
|
||||
const entry = currentStyleParts[i].split(':');
|
||||
styles[entry.splice(0, 1)[0]] = entry.join(':').trim();
|
||||
}
|
||||
}
|
||||
|
||||
styles = {...styles, ...style};
|
||||
const styleString = (
|
||||
Object.entries(styles).map(([k, v]) => `${k}:${v}`).join(';')
|
||||
);
|
||||
|
||||
containerRef.current?.setAttribute("style", `${styleString}`);
|
||||
}, []);
|
||||
|
||||
const onbeforeDropIntercept = (instance: BrowserJsPlumbInstance, params: BeforeDropParams) => {
|
||||
const existingConnections: ConnectionSelection = instance.select({ source: params.sourceId as any, target: params.targetId as any });
|
||||
|
||||
// prevent duplicates when switching existing connections
|
||||
if (existingConnections.length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingConnections.length > 0) {
|
||||
const firstConnection: Connection = {...existingConnections.get(0)} as Connection;
|
||||
|
||||
// special case to handle existing connections changing targets
|
||||
if (firstConnection.suspendedElementId) {
|
||||
onConnectionDetached([params.sourceId, firstConnection.suspendedElementId]);
|
||||
|
||||
if (params.targetId !== firstConnection.suspendedElementId) {
|
||||
onConnectionAttached([params.sourceId, params.targetId]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent duplicate connections from the same source to target
|
||||
if (firstConnection.sourceId === params.sourceId && firstConnection.targetId === params.targetId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent looping connections between a target and source
|
||||
const loopCheck = instance.select({ source: params.targetId as any, target: params.sourceId as any });
|
||||
if (loopCheck.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevent a connection from a target to itself
|
||||
if (params.sourceId === params.targetId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.reset();
|
||||
instance.destroy();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
||||
if (stateRef.current) {
|
||||
Object.values(stateRef.current).forEach((x) => {
|
||||
if (!instance.selectEndpoints({ element: x.key as any }).length) {
|
||||
const maxConnections = -1;
|
||||
const el = document.getElementById(x.key) as Element;
|
||||
|
||||
if (el) {
|
||||
addEndpoints(
|
||||
el,
|
||||
getAnchors(x.outputs, outputAnchors),
|
||||
getAnchors(x.inputs, inputAnchors),
|
||||
maxConnections
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
onGraphUpdate({
|
||||
'nodes': stateRef.current,
|
||||
'connections': getConnections(instance.getConnections({}, true) as Connection[])
|
||||
});
|
||||
}
|
||||
}, [instance, addEndpoints, onGraphUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
||||
let exisitngConnectionUuids = (instance.getConnections({}, true) as Connection[]).map(
|
||||
(x) => x.getUuids()
|
||||
);
|
||||
|
||||
connections.forEach((x) => {
|
||||
let c = exisitngConnectionUuids.find((y) => {
|
||||
return isEqual([`op_${x[0]}`, `ip_${x[1]}`], y)
|
||||
});
|
||||
|
||||
if (!c) {
|
||||
instance.connect({
|
||||
uuids: [`op_${x[0]}`, `ip_${x[1]}`],
|
||||
overlays: [getOverlayObject(instance)]
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [connections, instance]);
|
||||
|
||||
useEffect(() => {
|
||||
const jsPlumbInstance: BrowserJsPlumbInstance = newInstance({
|
||||
...defaultOptions,
|
||||
container: containerRef.current
|
||||
});
|
||||
|
||||
jsPlumbInstance.bind(INTERCEPT_BEFORE_DROP, function (params: BeforeDropParams) {
|
||||
return onbeforeDropIntercept(jsPlumbInstance, params);
|
||||
});
|
||||
|
||||
jsPlumbInstance.bind(EVENT_CONNECTION_DETACHED, function (this: BrowserJsPlumbInstance, params: ConnectionDetachedParams) {
|
||||
onConnectionDetached([params.sourceId, params.targetId]);
|
||||
|
||||
onGraphUpdate({
|
||||
'nodes': stateRef.current,
|
||||
'connections': getConnections(this.getConnections({}, true) as Connection[])
|
||||
});
|
||||
});
|
||||
|
||||
jsPlumbInstance.bind(EVENT_CONNECTION, function (this: BrowserJsPlumbInstance, params: ConnectionEstablishedParams) {
|
||||
if (!params.connection.overlays.hasOwnProperty("remove-conn")) {
|
||||
params.connection.addOverlay(getOverlayObject(this));
|
||||
onConnectionAttached([params.sourceId, params.targetId]);
|
||||
}
|
||||
|
||||
onGraphUpdate({
|
||||
'nodes': stateRef.current,
|
||||
'connections': getConnections(this.getConnections({}, true) as Connection[])
|
||||
});
|
||||
});
|
||||
|
||||
jsPlumbInstance.bind(EVENT_DRAG_STOP, (p: any) => {
|
||||
onEndpointPositionUpdate({
|
||||
key: p.el.id,
|
||||
position: {
|
||||
top: p.el.offsetTop,
|
||||
left: p.el.offsetLeft
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
jsPlumbInstance.bind(EVENT_CONNECTION_DBL_CLICK, (connection: Connection) => {
|
||||
jsPlumbInstance.deleteConnection(connection);
|
||||
});
|
||||
|
||||
/*
|
||||
jsPlumbInstance.bind("drag:move", function (info: any) {
|
||||
const parentRect = jsPlumbInstance.getContainer().getBoundingClientRect()
|
||||
const childRect = info.el.getBoundingClientRect()
|
||||
if (childRect.right > parentRect.right) info.el.style.left = `${parentRect.width - childRect.width}px`
|
||||
if (childRect.left < parentRect.left) info.el.style.left = '0px'
|
||||
if (childRect.top < parentRect.top) info.el.style.top = '0px'
|
||||
if (childRect.bottom > parentRect.bottom) info.el.style.top = `${parentRect.height - childRect.height}px`
|
||||
});
|
||||
*/
|
||||
|
||||
setInstance(jsPlumbInstance);
|
||||
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return [containerCallbackRef, setZoom, setStyle, removeEndpoint];
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export const API_SERVER_URL = process.env.REACT_APP_API_SERVER;
|
||||
export const PROJECTS_FETCH_LIMIT = 300;
|
||||
export const LOCAL_STORAGE = 'CtkLocalStorage';
|
||||
@ -0,0 +1,11 @@
|
||||
import { LOCAL_STORAGE } from "../constants"
|
||||
|
||||
|
||||
export const useLocalStorageAuth = () => {
|
||||
const localStorageData = localStorage.getItem(LOCAL_STORAGE);
|
||||
|
||||
if (localStorageData) {
|
||||
const authData = JSON.parse(localStorageData);
|
||||
return authData
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function getWindowDimensions() {
|
||||
const { innerWidth: width, innerHeight: height } = window;
|
||||
return {
|
||||
width,
|
||||
height
|
||||
};
|
||||
}
|
||||
|
||||
export default function useWindowDimensions() {
|
||||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
setWindowDimensions(getWindowDimensions());
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return windowDimensions;
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.jsplumb-box {
|
||||
background-size: 16px 16px;
|
||||
background-image:
|
||||
linear-gradient(to right, #80808014 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #80808014 1px, transparent 1px);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100vh - 64px); /* 64px is the bar above the canvas */
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
border-radius: 1em;
|
||||
width: 150px;
|
||||
height: 60px;
|
||||
z-index: 30;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.node-item img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.jtk-connector {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
path,
|
||||
.jtk-endpoint {
|
||||
z-index: 20;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node-item.jtk-drag {
|
||||
box-shadow: 0px 0px 5px 2px rgba(75, 0, 255, 0.37);
|
||||
}
|
||||
|
||||
.jtk-drag-select * {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.remove-conn-btn {
|
||||
background-color: #61B7CF;
|
||||
}
|
||||
|
||||
.remove-conn-btn:hover {
|
||||
background-color: #ce4551;
|
||||
}
|
||||
|
||||
.code-column {
|
||||
background-color: #1F2937;
|
||||
}
|
||||
|
||||
.cke_reset_all .CodeMirror-scroll * {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #464646 #282c34;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #464646;
|
||||
border-radius: 20px;
|
||||
border: 5px solid #282c34;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-util {
|
||||
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-indigo-500;
|
||||
}
|
||||
.btn-util-selected {
|
||||
@apply text-white bg-indigo-500 hover:bg-indigo-500 focus:ring-indigo-500;
|
||||
}
|
||||
.input-util {
|
||||
@apply shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md px-2 py-1 mt-1
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@ -0,0 +1,15 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
export type ProtectedRouteProps = {
|
||||
isAuthenticated: boolean;
|
||||
authenticationPath: string;
|
||||
outlet: JSX.Element;
|
||||
};
|
||||
|
||||
export default function ProtectedRoute({ isAuthenticated, authenticationPath, outlet }: ProtectedRouteProps) {
|
||||
if (isAuthenticated) {
|
||||
return outlet;
|
||||
} else {
|
||||
return <Navigate to={{ pathname: authenticationPath }} />;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useOutsideClick = (ref: any, callback: any) => {
|
||||
const handleClick = (e: any) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default useOutsideClick;
|
||||
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@ -0,0 +1,186 @@
|
||||
import { omit } from "lodash";
|
||||
|
||||
const RESET = "reset";
|
||||
|
||||
const PROJECT_NAME = "project-name";
|
||||
const CANVAS_POSITION = "canvas-position";
|
||||
|
||||
const ENDPOINT_ALL = "endpoints";
|
||||
const ENDPOINT_CREATED = "endpoint-created";
|
||||
const ENDPOINT_UPDATED = "endpoint-updated";
|
||||
const ENDPOINT_DELETED = "endpoint-deleted";
|
||||
|
||||
const CONNECTIONS_ALL = "connections";
|
||||
const CONNECTION_DETACHED = "connection-detached";
|
||||
const CONNECTION_ATTACHED = "connection-attached";
|
||||
|
||||
const AUTH_LOGIN_SUCCESS = "auth-login-success";
|
||||
const AUTH_LOGOUT_SUCCESS = "auth-logout-success";
|
||||
const AUTH_SELF = "auth-self"
|
||||
|
||||
const getMatchingSetIndex = (setOfSets: [[string, string]], findSet: [string, string]): number => {
|
||||
return setOfSets.findIndex((set) => set.toString() === findSet.toString());
|
||||
}
|
||||
|
||||
export const initialState = {
|
||||
projectName: "",
|
||||
nodes: {},
|
||||
connections: [],
|
||||
canvasPosition: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
export const reducer = (state: any, action: any) => {
|
||||
let existingIndex;
|
||||
let _connections;
|
||||
|
||||
switch (action.type) {
|
||||
case RESET:
|
||||
return {
|
||||
...initialState
|
||||
}
|
||||
|
||||
case PROJECT_NAME:
|
||||
return {
|
||||
...state,
|
||||
projectName: action.payload
|
||||
}
|
||||
case CANVAS_POSITION:
|
||||
return {
|
||||
...state,
|
||||
canvasPosition: {...state.canvasPosition, ...action.payload}
|
||||
}
|
||||
case ENDPOINT_ALL:
|
||||
return {
|
||||
...state,
|
||||
nodes: action.payload
|
||||
}
|
||||
case ENDPOINT_CREATED:
|
||||
return {
|
||||
...state,
|
||||
nodes: {...state.nodes, [action.payload.key]: action.payload}
|
||||
}
|
||||
case ENDPOINT_DELETED:
|
||||
return {
|
||||
...state,
|
||||
nodes: {...omit(state.nodes, action.payload.key)}
|
||||
}
|
||||
case ENDPOINT_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
nodes: {...state.nodes, [action.payload.key]: action.payload}
|
||||
}
|
||||
case CONNECTIONS_ALL:
|
||||
return {
|
||||
...state,
|
||||
connections: action.payload.map((x: any) => x)
|
||||
}
|
||||
case CONNECTION_DETACHED:
|
||||
_connections = state.connections;
|
||||
existingIndex = getMatchingSetIndex(_connections, action.payload);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
_connections.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: [..._connections]
|
||||
}
|
||||
case CONNECTION_ATTACHED:
|
||||
_connections = state.connections;
|
||||
existingIndex = getMatchingSetIndex(state.connections, action.payload);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
_connections.push(action.payload);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: [..._connections]
|
||||
}
|
||||
case AUTH_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: { ...action.payload.user }
|
||||
}
|
||||
case AUTH_SELF:
|
||||
return {
|
||||
...state,
|
||||
user: { ...action.payload }
|
||||
}
|
||||
case AUTH_LOGOUT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: null
|
||||
}
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
}
|
||||
|
||||
export const updateProjectName = (data: string) => ({
|
||||
type: PROJECT_NAME,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const position = (data: any) => ({
|
||||
type: CANVAS_POSITION,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const connections = (data: any) => ({
|
||||
type: CONNECTIONS_ALL,
|
||||
payload: data || []
|
||||
});
|
||||
|
||||
export const connectionDetached = (data: any) => ({
|
||||
type: CONNECTION_DETACHED,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const connectionAttached = (data: any) => ({
|
||||
type: CONNECTION_ATTACHED,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const nodes = (data: any) => ({
|
||||
type: ENDPOINT_ALL,
|
||||
payload: data || {}
|
||||
});
|
||||
|
||||
export const nodeCreated = (data: any) => ({
|
||||
type: ENDPOINT_CREATED,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const nodeUpdated = (data: any) => ({
|
||||
type: ENDPOINT_UPDATED,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const nodeDeleted = (data: any) => ({
|
||||
type: ENDPOINT_DELETED,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const authLoginSuccess = (data: any) => ({
|
||||
type: AUTH_LOGIN_SUCCESS,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const authLogoutSuccess = () => ({
|
||||
type: AUTH_LOGOUT_SUCCESS,
|
||||
});
|
||||
|
||||
export const authSelf = (data: any) => ({
|
||||
type: AUTH_SELF,
|
||||
payload: data
|
||||
});
|
||||
|
||||
export const resetState = () => ({
|
||||
type: RESET
|
||||
})
|
||||
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@ -0,0 +1,44 @@
|
||||
import { API_SERVER_URL } from "../constants";
|
||||
import { getLocalStorageJWTKeys } from "./utils";
|
||||
|
||||
export const signup = (username: string, email: string, password1: string, password2: string) =>
|
||||
fetch(`${API_SERVER_URL}/auth/registration/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ username, email, password1, password2 })
|
||||
});
|
||||
|
||||
export const logIn = (username: string, password: string) =>
|
||||
fetch(`${API_SERVER_URL}/auth/login/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
export const self = () => {
|
||||
const jwtKeys = getLocalStorageJWTKeys();
|
||||
return fetch(`${API_SERVER_URL}/auth/self/`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const refresh = () => {
|
||||
const jwtKeys = getLocalStorageJWTKeys();
|
||||
const body = { "refresh": jwtKeys.refresh_token };
|
||||
|
||||
return fetch(`${API_SERVER_URL}/auth/token/refresh/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { IGeneratePayload } from "../types";
|
||||
import { API_SERVER_URL } from "../constants";
|
||||
|
||||
export const generateHttp = (data: IGeneratePayload) => {
|
||||
return fetch(`${API_SERVER_URL}/generate/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
export const checkHttpStatus = (response: any) => {
|
||||
if ([200, 201, 202].includes(response.status)) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return response;
|
||||
}
|
||||
|
||||
throw response;
|
||||
}
|
||||
|
||||
export const checkHttpSuccess = (response: any) => {
|
||||
if ([200, 201, 202].includes(response.status)) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return response;
|
||||
}
|
||||
|
||||
throw response;
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { IProjectPayload } from "../types";
|
||||
import { API_SERVER_URL, PROJECTS_FETCH_LIMIT } from "../constants";
|
||||
import { getLocalStorageJWTKeys } from "./utils";
|
||||
|
||||
export const projectHttpCreate = (data: string) => {
|
||||
//const jwtKeys = getLocalStorageJWTKeys();
|
||||
return fetch(`${API_SERVER_URL}/projects/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
//"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
},
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
export const projectHttpUpdate = (uuid: string, data: string) => {
|
||||
//const jwtKeys = getLocalStorageJWTKeys();
|
||||
return fetch(`${API_SERVER_URL}/projects/${uuid}/`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
//"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
},
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
export const projectHttpDelete = (uuid: number) => {
|
||||
const jwtKeys = getLocalStorageJWTKeys();
|
||||
return fetch(`${API_SERVER_URL}/projects/${uuid}/`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const projectsHttpGet = (offset: number) => {
|
||||
const jwtKeys = getLocalStorageJWTKeys();
|
||||
let endpoint = `${API_SERVER_URL}/projects/?limit=${PROJECTS_FETCH_LIMIT}&offset=${offset}`;
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const projectHttpGet = (uuid: string) => {
|
||||
//const jwtKeys = getLocalStorageJWTKeys();
|
||||
return fetch(`${API_SERVER_URL}/projects/${uuid}/`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
//"Authorization": `Bearer ${jwtKeys.access_token}`
|
||||
}
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue