feat: scaffolding React rewrite, work-in-progress

pull/68/head
Artem Golub 3 years ago committed by Samuel Rowe
parent a3e9ec0486
commit bb52610142

@ -1,5 +0,0 @@
.git
Dockerfile
.DS_Store
README.md
env.*

209
.gitignore vendored

@ -1,51 +1,3 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -68,10 +20,12 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
*.pickle
MANIFEST
# PyInstaller
@ -97,7 +51,6 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
@ -120,7 +73,6 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@ -131,9 +83,7 @@ profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -179,8 +129,153 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
local_packages
tmp
*.lock
# AWS
*.iml
.idea/
.aws-sam/
*/dist/*
packaged.yaml
package-lock.json
# local
cache
keys
# Node
# https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Cython debug symbols
cython_debug/
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

@ -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,6 +1,6 @@
# Nuxx Visual Docker Composer
# Container ToolKit
## Local setup
## Local setup and development
On a Mac/Linux/Windows you need Docker, Docker Compose installed. Optionally GCC to run make commands for convenience, or just run the commands from the Makefile by hand.
@ -8,20 +8,15 @@ To get the tool working locally, just run:
```shell script
$ make local_setup
$ make run_server
$ cd services/frontend && npm run start
```
... this command will bring up the backend, the database, sync migrations, and build and serve the Angular app in an Nginx container (for working locally with the tool). For production, you can build and deploy your own images or use mine as a base.
## Local development
- You can run the backend in dev mode with `make backend_dev`.
- For developing the frontend run `cd ./src/composer && npm run start`. It will expect the backend connection on http://localhost:9001/api, but you can change this to your liking in src/composer/src/environment/environment.ts.
... this command will bring up the backend, the database, sync migrations,
## Project roadmap
- Complete react rewrite.
- Ongoing improvements and features for docker compose yaml generation.
- Kubernetes yaml generation.
- Application stack deployments directly from the web tool and CLI.
- Nuxx CLI.
For anynone interested on trying out deployments, and the CLI, please message me on Slack. These features need some more work and testing.
- Kubernetes manifest generation.
- Deployment to user's ECS, K8S, GS accounts.

@ -3,61 +3,64 @@ version: "3.4"
volumes:
postgres-data:
driver: local
name: nuxx_postgres_data
name: ctk_postgres_data
django-static:
driver: local
name: nuxx_django_static
name: ctk_django_static
services:
postgres:
container_name: nuxx-postgres
image: postgres:9.6.1
container_name: ctk-postgres
image: postgres:11
ports:
- 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 30s
timeout: 30s
retries: 3
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
backend:
container_name: nuxx-api
container_name: ctk-server
restart: always
build: src/backend
image: nuxxapp/nuxx-api:1.0.0
build:
context: ./
dockerfile: ./services/backend/Dockerfile
image: corpulent/ctk-api:1.0.0
working_dir: /home
depends_on:
- postgres
links:
- postgres
volumes:
- ./src/backend/server:/home/app/
- ./services/backend/src:/home/server/
- django-static:/static/
ports:
- "9001:9001"
env_file:
- local.env
environment:
- DB_REMOTE=False
nginx:
container_name: nuxx-nginx
frontend:
container_name: ctk-frontend
restart: always
image: nginx:latest
build:
context: ./
dockerfile: ./services/frontend/Dockerfile
image: corpulent/ctk-frontend:1.0.0
depends_on:
- backend
links:
- backend
volumes:
# configs
- ${PWD}/configs/nginx/uwsgi_params:/home/config/uwsgi/uwsgi_params
- ${PWD}/configs/nginx/localhost.conf:/etc/nginx/conf.d/default.conf
- ${PWD}/configs/nginx/nginx.conf:/etc/nginx/nginx.conf
- ${PWD}/services/frontend/configs/nginx/uwsgi_params:/home/config/uwsgi/uwsgi_params
- ${PWD}/services/frontend/configs/nginx/localhost.conf:/etc/nginx/conf.d/default.conf
- ${PWD}/services/frontend/configs/nginx/nginx.conf:/etc/nginx/nginx.conf
# serve django static stuff
- django-static:/home/backend/static/
- django-static:/home/server/static/
# serve composer "built" angular app
- ./src/composer/dist/frontend:/usr/share/nginx/html/
# serve composer built react app
- ${PWD}/services/frontend/build:/usr/share/nginx/html/
ports:
- "80:80"
- "80:80"

@ -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=

BIN
services/.DS_Store vendored

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

@ -7,7 +7,7 @@ processes = 5
[base]
chdir = /home/app
chdir = /home/server
module = server.wsgi:application
chmod-socket=666
uid = uwsgi

@ -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)

@ -2,4 +2,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

@ -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'],
},
),
]

@ -1,4 +1,4 @@
# Generated by Django 3.0.4 on 2020-05-31 13:33
# Generated by Django 4.0.4 on 2022-06-22 10:17
from django.db import migrations, models
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
operations = [
migrations.AddField(
model_name='project',
name='mutable',
field=models.BooleanField(default=True),
name='uuid',
field=models.CharField(blank=True, max_length=500, null=True, unique=True),
),
]

@ -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)

@ -1,34 +1,18 @@
import io
import os
import re
import sys
import json
import boto3
import copy
import random
import string
import stat
import decimal
import time
import pyaml
import docker
import yaml
import ast
import uuid
import ruamel.yaml
import contextlib
import docker
from better_profanity import profanity
from io import StringIO
from collections import OrderedDict
from time import mktime
from datetime import date, datetime
from operator import itemgetter
from botocore.exceptions import ClientError
from ruamel.yaml import YAML
from ruamel.yaml.tokens import CommentToken
from ruamel.yaml.comments import CommentedMap, CommentedSeq
from ruamel.yaml.scalarstring import PreservedScalarString, SingleQuotedScalarString, DoubleQuotedScalarString
from ruamel.yaml.scalarstring import SingleQuotedScalarString, DoubleQuotedScalarString
from api.models import Project
try:
@ -41,7 +25,18 @@ except AttributeError: # undefined function (wasn't added until Python 3.3)
else:
def indent(text, amount, ch=' '):
return textwrap.indent(text, amount * ch)
def get_project_obj_by_id(id):
with contextlib.suppress(Project.DoesNotExist):
return Project.objects.get(pk=id)
return None
def get_project_obj_by_uuid(uuid):
with contextlib.suppress(Project.DoesNotExist):
return Project.objects.get(uuid=uuid)
return None
def sequence_indent_four(s):
ret_val = ''
@ -83,7 +78,7 @@ def sequence_indent_one(s):
def format_quotes(s):
if '\'' in s:
return SingleQuotedScalarString(s.replace("'", ''))
return SingleQuotedScalarString(s.replace("'", ''))
if '"' in s:
return DoubleQuotedScalarString(s.replace('"', ''))
@ -110,18 +105,16 @@ def format_volumes_top_level(volumes, compose_version):
if volume_custom_name:
ret[volume['name']]['name'] = volume_custom_name
if volume_driver:
ret[volume['name']]['driver'] = volume_driver
if compose_version in [2, 3]:
labels = volume.get('labels', None)
if labels:
if labels := volume.get('labels', None):
ret[volume['name']]['labels'] = {}
for label in labels:
ret[volume['name']]['labels'][label['key']] = format_quotes(label['value'])
if not ret[volume['name']]:
ret[volume['name']] = None
@ -146,18 +139,17 @@ def format_networks_top_level(networks, compose_version):
if network_custom_name:
ret[network['name']]['name'] = network_custom_name
if network_driver:
ret[network['name']]['driver'] = network_driver
if driver_opts:
ret[network['name']]['driver_opts'] = {}
for driver_opt in driver_opts:
ret[network['name']]['driver_opts'][driver_opt['key']] = format_quotes(driver_opt['value'])
if compose_version in [2, 3]:
labels = network.get('labels', None)
if labels:
if labels := network.get('labels', None):
ret[network['name']]['labels'] = {}
for label in labels:
ret[network['name']]['labels'][label['key']] = format_quotes(label['value'])
@ -169,12 +161,7 @@ def format_networks_top_level(networks, compose_version):
def format_key_val_pairs(pairs):
ret = {}
for pair_part in pairs:
ret[pair_part['key']] = pair_part['value']
return ret
return {pair_part['key']: pair_part['value'] for pair_part in pairs}
def format_ports(ports):
@ -196,11 +183,10 @@ def format_volumes(service_volumes, volumes):
ret = []
for service_volume in service_volumes:
for volume in volumes:
if 'volume' in service_volume:
if service_volume['volume'] == volume['uuid']:
volume_mount_str = f"{volume['name']}:{service_volume['destination']}"
ret.append(volume_mount_str)
if 'volume' in service_volume and service_volume['volume'] == volume['uuid']:
volume_mount_str = f"{volume['name']}:{service_volume['destination']}"
ret.append(volume_mount_str)
if 'relativePathSource' in service_volume:
volume_mount_str = f"{service_volume['relativePathSource']}:{service_volume['destination']}"
ret.append(volume_mount_str)
@ -232,7 +218,6 @@ def format_command_string(command):
"""
Format command list of string for v1, v2, v3.
param: command: string
return: list
"""
command_list = []
@ -243,14 +228,12 @@ def format_command_string(command):
# try to convert the string into list
command_list = ast.literal_eval(command_list)
except (ValueError, SyntaxError) as e:
#print('ValueError SyntaxError', e)
# special case
if "\n" in command:
command_list = command.split("\n")
else:
return command
except Exception as e:
#print('Exception', e)
return command
if len(command_list) > 1:
@ -270,6 +253,7 @@ def format_build(specified_version, build):
build_str = build.get('build', None)
context_str = build.get('context', None)
ret = {}
if specified_version < 2:
if build_str:
@ -279,11 +263,9 @@ def format_build(specified_version, build):
else:
return None
if build_str:
return build_str
ret = {}
for _key, _val in build.items():
if _key in ['args', 'cache_from', 'labels']:
if _val:
@ -304,7 +286,7 @@ def _remove_missing_and_underscored_keys(d):
del d[key]
elif isinstance(d[key], dict):
d[key] = _remove_missing_and_underscored_keys(d[key])
if d[key] == None or d[key] == {}:
if d[key] is None or d[key] == {}:
del d[key]
return d
@ -313,18 +295,12 @@ def _remove_missing_and_underscored_keys(d):
def format_deploy(specified_version, deploy):
ret = deploy
try:
with contextlib.suppress(Exception):
placement_preferences = deploy['placement']['preferences']
ret['placement']['preferences'] = format_key_val_pairs(placement_preferences)
except Exception:
pass
try:
with contextlib.suppress(Exception):
labels = deploy['labels']
ret['labels'] = format_key_val_pairs(labels)
except Exception:
pass
ret = _remove_missing_and_underscored_keys(ret)
return ret
@ -342,9 +318,7 @@ def format_services_version_one(specified_version, services, volumes, networks):
for service in services:
service_formatted = {}
image = service.get('image', None)
if image:
if image := service.get('image', None):
image_tag = "latest"
try:
@ -353,92 +327,84 @@ def format_services_version_one(specified_version, services, volumes, networks):
except KeyError:
service_formatted['image'] = f"{image}"
try:
with contextlib.suppress(KeyError):
if service['container_name']:
service_formatted['container_name'] = service['container_name']
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['restart']:
service_formatted['restart'] = service['restart']
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['command']:
service_formatted['command'] = format_command_string(service['command'])
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['entrypoint']:
service_formatted['entrypoint'] = format_command_string(service['entrypoint'])
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['working_dir']:
service_formatted['working_dir'] = service['working_dir']
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['ports']:
service_formatted['ports'] = format_ports(service['ports'])
except KeyError:
pass
try:
links = service.get('links', [])
if links:
with contextlib.suppress(KeyError):
if links := service.get('links', []):
service_formatted['links'] = []
for link in links:
for service_obj in services:
if link == service_obj['uuid']:
service_formatted['links'].append(f"{service_obj['name']}")
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['environment']:
envs = service['environment']
service_formatted['environment'] = format_key_val_pairs(envs)
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
service_volumes = service['volumes']
formatted_volumes = format_volumes(service_volumes, volumes)
if formatted_volumes:
if formatted_volumes := format_volumes(service_volumes, volumes):
service_formatted['volumes'] = formatted_volumes
else:
del service_formatted['volumes']
except KeyError:
pass
try:
build = format_build(specified_version, service['build'])
if build:
with contextlib.suppress(KeyError):
if build := format_build(specified_version, service['build']):
service_formatted['build'] = build
except KeyError:
pass
services_formatted[service['name']] = service_formatted
return services_formatted
def format_services_version_three(specified_version, services, volumes, networks):
def get_service_by_label_key(key, services):
for service in services:
with contextlib.suppress(KeyError):
if key == service["labels"]["key"]:
return service
return None
def get_connected_services(service_key, connections, services):
connected_services = []
for connection in connections:
if service_key == connection[0]:
if connected_service := get_service_by_label_key(connection[1], services):
connected_services.append(connected_service)
return connected_services
def format_services_version_three(specified_version, services, connections, volumes, networks):
services_formatted = {}
for service in services:
service_formatted = {}
image = service.get('image', None)
if image:
service_key = ""
# add labels excluding certain keys
if labels := service.get('labels', {}):
clean_labels = {x: labels[x] for x in labels if x not in ["key"]}
if "key" in labels:
service_key = labels["key"]
if bool(clean_labels):
service_formatted['labels'] = clean_labels
# image name
if image := service.get('image', None):
image_tag = "latest"
try:
@ -447,108 +413,53 @@ def format_services_version_three(specified_version, services, volumes, networks
except KeyError:
service_formatted['image'] = f"{image}"
try:
# dependencies
with contextlib.suppress(KeyError):
if connected_services := get_connected_services(service_key, connections, services):
service_formatted['depends_on'] = []
for connected_service in connected_services:
service_formatted['depends_on'].append(f"{connected_service['name']}")
with contextlib.suppress(KeyError):
if service['container_name']:
service_formatted['container_name'] = service['container_name']
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['restart']:
service_formatted['restart'] = service['restart']
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['command']:
service_formatted['command'] = format_command_string(service['command'])
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['entrypoint']:
service_formatted['entrypoint'] = format_command_string(service['entrypoint'])
except KeyError:
pass
try:
working_dir_str = service['working_dir']
if working_dir_str:
with contextlib.suppress(KeyError):
if working_dir_str := service['working_dir']:
service_formatted['working_dir'] = working_dir_str
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['ports']:
service_formatted['ports'] = format_ports(service['ports'])
except KeyError:
pass
try:
if service['depends_on']:
depends_on = service['depends_on']
service_formatted['depends_on'] = []
for depends in depends_on:
for service_obj in services:
if depends == service_obj['uuid']:
service_formatted['depends_on'].append(f"{service_obj['name']}")
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
if service['environment']:
envs = service['environment']
service_formatted['environment'] = format_key_val_pairs(envs)
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
service_volumes = service['volumes']
formatted_volumes = format_volumes(service_volumes, volumes)
if formatted_volumes:
if formatted_volumes := format_volumes(service_volumes, volumes):
service_formatted['volumes'] = formatted_volumes
else:
del service_formatted['volumes']
except KeyError:
pass
try:
labels = service.get('labels', None)
if labels:
service_formatted['labels'] = {}
for label in labels:
service_formatted['labels'][label['key']] = format_quotes(label['value'])
except KeyError:
pass
try:
with contextlib.suppress(KeyError):
service_networks = service.get('networks', [])
formatted_networks = format_networks(service_networks, networks)
if formatted_networks:
if formatted_networks := format_networks(service_networks, networks):
service_formatted['networks'] = formatted_networks
else:
del service_formatted['networks']
except KeyError:
pass
try:
build = format_build(specified_version, service['build'])
if build:
with contextlib.suppress(KeyError):
if build := format_build(specified_version, service['build']):
service_formatted['build'] = build
except KeyError:
pass
if int(float(specified_version)) >= 3:
try:
deploy = format_deploy(specified_version, service['deploy'])
if deploy:
with contextlib.suppress(KeyError):
if deploy := format_deploy(specified_version, service['deploy']):
service_formatted['deploy'] = deploy
except KeyError:
pass
services_formatted[service['name']] = service_formatted
return services_formatted
@ -563,54 +474,55 @@ def FSlist(l): # concert list into flow-style (default is block style)
return cs
def generate_dc(services, volumes, networks, secrets, configs, version="3", return_format='yaml'):
if return_format == 'yaml':
s = io.StringIO()
ret_yaml = YAML()
ret_yaml.indent(mapping=2, sequence=4, offset=2)
ret_yaml.explicit_start = True
specified_version = get_version(version)
base_version = int(specified_version)
if services:
if base_version in [2, 3]:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_three(specified_version, services, volumes, networks)
ret_yaml.dump({'services': services_formatted}, s, transform=sequence_indent_four)
if base_version == 1:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_one(specified_version, services, volumes, networks)
ret_yaml.dump(services_formatted, s, transform=sequence_indent_one)
s.write('\n')
def generate_dc(services, connections, volumes, networks, secrets, configs, version="3", return_format='yaml'):
if return_format != 'yaml':
return
if base_version in [3, 2]:
if networks:
networks_formatted = format_networks_top_level(networks, version)
ret_yaml.dump({'networks': networks_formatted}, s)
s.write('\n')
s = io.StringIO()
ret_yaml = YAML()
ret_yaml.indent(mapping=2, sequence=4, offset=2)
ret_yaml.explicit_start = True
specified_version = get_version(version)
base_version = int(specified_version)
if volumes:
volumes_formatted = format_volumes_top_level(volumes, version)
ret_yaml.dump({'volumes': volumes_formatted}, s)
s.write('\n')
if secrets:
ret_yaml.dump({'secrets': secrets}, s)
if services:
if base_version in {2, 3}:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_three(specified_version, services, connections, volumes, networks)
ret_yaml.dump({'services': services_formatted}, s, transform=sequence_indent_four)
if configs:
ret_yaml.dump({'configs': configs}, s)
if base_version == 1:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_one(specified_version, services, volumes, networks)
ret_yaml.dump(services_formatted, s, transform=sequence_indent_one)
s.write('\n')
if base_version in {3, 2} and networks:
networks_formatted = format_networks_top_level(networks, version)
ret_yaml.dump({'networks': networks_formatted}, s)
s.write('\n')
if volumes:
volumes_formatted = format_volumes_top_level(volumes, version)
ret_yaml.dump({'volumes': volumes_formatted}, s)
s.write('\n')
s.seek(0)
if secrets:
ret_yaml.dump({'secrets': secrets}, s)
s.write('\n')
return s
if configs:
ret_yaml.dump({'configs': configs}, s)
s.write('\n')
s.seek(0)
return s
def generate(cname):
@ -619,15 +531,11 @@ def generate(cname):
try:
cid = [x.short_id for x in c.containers.list() if cname == x.name or x.short_id in cname][0]
except IndexError:
print("That container is not running.")
sys.exit(1)
cattrs = c.containers.get(cid).attrs
# Build yaml dict structure
cfile = {}
networks = {}
cfile[cattrs['Name'][1:]] = {}
ct = cfile[cattrs['Name'][1:]]
@ -672,7 +580,6 @@ def generate(cname):
}
networklist = c.networks.list()
networks = {}
for network in networklist:
if network.attrs['Name'] in values['networks'].keys():
networks[network.attrs['Name']] = {'external': (not network.attrs['Internal'])}
@ -687,7 +594,7 @@ def generate(cname):
ports_value = [cattrs['HostConfig']['PortBindings'][key][0]['HostIp']+':'+cattrs['HostConfig']['PortBindings'][key][0]['HostPort']+':'+key for key in cattrs['HostConfig']['PortBindings']]
# If bound ports found, don't use the 'expose' value.
if (ports_value != None) and (ports_value != "") and (ports_value != []) and (ports_value != 'null') and (ports_value != {}) and (ports_value != "default") and (ports_value != 0) and (ports_value != ",") and (ports_value != "no"):
if ports_value not in [None, "", [], 'null', {}, "default", 0, ",", "no"]:
for index, port in enumerate(ports_value):
if port[0] == ':':
ports_value[index] = port[1:]
@ -701,9 +608,8 @@ def generate(cname):
ports = None
# Iterate through values to finish building yaml dict.
for key in values:
value = values[key]
if (value != None) and (value != "") and (value != []) and (value != 'null') and (value != {}) and (value != "default") and (value != 0) and (value != ",") and (value != "no"):
for key, value in values.items():
if value not in [None, "", [], 'null', {}, "default", 0, ",", "no"]:
ct[key] = value
return cfile, networks
@ -716,7 +622,6 @@ def generate_uuid():
def random_string(string_length=10):
"""
Generate a random string of fixed length
:param string_length: integer
:return: string
"""
@ -730,8 +635,8 @@ def random_string(string_length=10):
def generate_rand_string():
rand_string = "".join(random.choice(
string.ascii_uppercase +
string.ascii_lowercase +
string.digits) for _ in range(16))
return rand_string
return "".join(
random.choice(
string.ascii_uppercase + string.ascii_lowercase + string.digits
) for _ in range(16)
)

@ -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()

@ -5,7 +5,8 @@ import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

@ -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;"]

@ -12,7 +12,7 @@ server {
charset utf-8;
location /static {
alias /home/backend/static;
alias /home/server/static;
}
location /admin {

@ -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…
Cancel
Save