diff --git a/FasterRunner/customer_swagger.py b/FasterRunner/customer_swagger.py new file mode 100644 index 00000000..798c6333 --- /dev/null +++ b/FasterRunner/customer_swagger.py @@ -0,0 +1,8 @@ +from drf_yasg.inspectors import SwaggerAutoSchema + + +class CustomSwaggerAutoSchema(SwaggerAutoSchema): + def get_tags(self, operation_keys=None): + if hasattr(self.view, 'swagger_tag'): + return [self.view.swagger_tag] + return super().get_tags(operation_keys) diff --git a/FasterRunner/settings/base.py b/FasterRunner/settings/base.py index 99d6f1e8..e6ca6baf 100644 --- a/FasterRunner/settings/base.py +++ b/FasterRunner/settings/base.py @@ -56,6 +56,7 @@ "drf_yasg", "system", "django_auth_ldap", + "mock", ] MIDDLEWARE = [ @@ -314,6 +315,11 @@ "level": "INFO", "propagate": True, }, + "mock": { + "handlers": ["default", "console", "error", "db"], + "level": "INFO", + "propagate": True, + }, "django_auth_ldap": { "handlers": ["default", "console", "error", "db"], "level": "INFO", diff --git a/FasterRunner/settings/dev.py b/FasterRunner/settings/dev.py index 7ffe1a0d..1969331a 100644 --- a/FasterRunner/settings/dev.py +++ b/FasterRunner/settings/dev.py @@ -6,6 +6,8 @@ DEBUG = True +# LOGGING["loggers"]["mock"]["level"] = "DEBUG" + logger.remove() logger.add( @@ -22,7 +24,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "fast_dev", # 新建数据库 + "NAME": "fast", # 新建数据库 # 'NAME': 'fast_mb4', # 新建数据库名 "HOST": "127.0.0.1", "USER": "root", # 数据库登录名 @@ -39,7 +41,7 @@ BROKER_URL = "amqp://username:password@localhost:5672//" # 需要先在RabbitMQ上创建fast_dev这个vhost -broker_url = 'amqp://admin:111111@192.168.22.19:5672/fast_dev' +broker_url = "amqp://admin:111111@192.168.22.19:5672/fast_dev" BASE_REPORT_URL = "http://localhost:8000/api/fastrunner/reports" diff --git a/FasterRunner/urls.py b/FasterRunner/urls.py index dc7b08b2..33d5a427 100644 --- a/FasterRunner/urls.py +++ b/FasterRunner/urls.py @@ -22,6 +22,7 @@ from rest_framework_jwt.views import obtain_jwt_token from fastrunner.views import run_all_auto_case +from mock.views import MockAPIView, MockAPIViewset, MockProjectViewSet from system import views as system_views schema_view = get_schema_view( @@ -40,7 +41,16 @@ system_router = DefaultRouter() system_router.register(r"log_records", system_views.LogRecordViewSet) +mock_api_router = DefaultRouter() +mock_api_router.register(r"mock_api", MockAPIViewset) + + +mock_project_router = DefaultRouter() +mock_project_router.register(r"mock_project", MockProjectViewSet) + urlpatterns = [ + path("api/mock/", include(mock_project_router.urls)), + path("api/mock/", include(mock_api_router.urls)), path(r"login", obtain_jwt_token), path("admin/", admin.site.urls), # re_path(r'^docs/', schema_view, name="docs"), @@ -62,4 +72,5 @@ re_path(r"^swagger(?P\.json|\.yaml)$", schema_view.without_ui(cache_timeout=0), name="schema-json"), path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + re_path(r'^mock/(?P\w+)(?P/.*)$', MockAPIView.as_view()) ] diff --git a/db/init.sql b/db/init.sql index 16927488..8641e2ab 100644 --- a/db/init.sql +++ b/db/init.sql @@ -15,10 +15,89 @@ /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +DROP TABLE IF EXISTS `mock_api_log`; +create table mock_api_log +( + request_obj longtext collate utf8mb4_bin not null + check (json_valid(`request_obj`)), + response_obj longtext collate utf8mb4_bin null + check (json_valid(`response_obj`)), + api_id varchar(32) not null, + project_id varchar(100) null, + request_id varchar(100) null, + id bigint auto_increment + primary key, + create_time datetime(6) not null, + update_time datetime(6) not null, + creator varchar(20) null, + updater varchar(20) null +); + +create index mock_api_log_api_id_d69785e2 + on mock_api_log (api_id); + +create index mock_api_log_project_id_e3b47bfd + on mock_api_log (project_id); + +create index mock_api_log_request_id_86906b7c + on mock_api_log (request_id); + +DROP TABLE IF EXISTS `mock_api_tab`; + +create table mock_api_tab +( + id bigint auto_increment + primary key, + request_path varchar(100) not null, + request_method varchar(10) not null, + request_body longtext collate utf8mb4_bin default json_object() null, + response_text longtext not null, + is_active tinyint(1) not null, + project_id varchar(100) null, + api_desc varchar(100) null, + api_id varchar(32) not null, + api_name varchar(100) null, + enabled tinyint(1) not null, + create_time datetime(6) not null, + update_time datetime(6) not null, + creator varchar(20) null, + updater varchar(20) null, + constraint api_id + unique (api_id), + constraint mock_api_tab_project_id_request_path__eeea3f07_uniq + unique (project_id, request_path, request_method) +); + +create index mock_api_tab_project_id_9b708a91 + on mock_api_tab (project_id); + +DROP TABLE IF EXISTS `mock_project_tab`; + +create table mock_project_tab +( + id bigint auto_increment + primary key, + project_id varchar(100) not null, + project_name varchar(100) not null, + project_desc varchar(100) not null, + is_active tinyint(1) not null, + create_time datetime(6) not null, + update_time datetime(6) not null, + creator varchar(20) null, + updater varchar(20) null, + constraint mock_project_tab_project_id_446f3335_uniq + unique (project_id), + constraint project_id + unique (project_id) +); + + + -- -- Table structure for table `api` -- + DROP TABLE IF EXISTS `api`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; diff --git a/mock/__init__.py b/mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mock/admin.py b/mock/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/mock/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/mock/apps.py b/mock/apps.py new file mode 100644 index 00000000..863318a3 --- /dev/null +++ b/mock/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MockConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "mock" diff --git a/mock/migrations/0001_initial.py b/mock/migrations/0001_initial.py new file mode 100644 index 00000000..1f79d3e4 --- /dev/null +++ b/mock/migrations/0001_initial.py @@ -0,0 +1,114 @@ +# Generated by Django 4.1.13 on 2024-02-25 21:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="MockProject", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ( + "creator", + models.CharField(max_length=20, null=True, verbose_name="创建人"), + ), + ( + "updater", + models.CharField(max_length=20, null=True, verbose_name="更新人"), + ), + ("project_id", models.CharField(max_length=100, unique=True)), + ("project_name", models.CharField(max_length=100)), + ("project_desc", models.CharField(max_length=100)), + ("is_active", models.BooleanField(default=True)), + ], + options={ + "verbose_name": "mock项目表", + "db_table": "mock_project_tab", + "unique_together": {("project_id",)}, + }, + ), + migrations.CreateModel( + name="MockAPI", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="创建时间"), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + ( + "creator", + models.CharField(max_length=20, null=True, verbose_name="创建人"), + ), + ( + "updater", + models.CharField(max_length=20, null=True, verbose_name="更新人"), + ), + ("request_path", models.CharField(max_length=100)), + ( + "request_method", + models.CharField( + choices=[ + ("GET", "GET"), + ("POST", "POST"), + ("PUT", "PUT"), + ("DELETE", "DELETE"), + ("PATCH", "PATCH"), + ], + default="GET", + max_length=10, + ), + ), + ("response_text", models.TextField()), + ("is_active", models.BooleanField(default=True)), + ( + "project", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to="mock.mockproject", + ), + ), + ], + options={ + "verbose_name": "mock接口表", + "db_table": "mock_api_tab", + "unique_together": {("project", "request_path", "request_method")}, + }, + ), + ] diff --git a/mock/migrations/0002_alter_mockapi_options_alter_mockapi_project.py b/mock/migrations/0002_alter_mockapi_options_alter_mockapi_project.py new file mode 100644 index 00000000..ef565e3f --- /dev/null +++ b/mock/migrations/0002_alter_mockapi_options_alter_mockapi_project.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.13 on 2024-02-27 22:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("mock", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="mockapi", + options={"ordering": ["-create_time"], "verbose_name": "mock接口表"}, + ), + migrations.AlterField( + model_name="mockapi", + name="project", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to="mock.mockproject", + to_field="project_id", + ), + ), + ] diff --git a/mock/migrations/0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more.py b/mock/migrations/0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more.py new file mode 100644 index 00000000..65491afd --- /dev/null +++ b/mock/migrations/0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.1.13 on 2024-02-27 22:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("mock", "0002_alter_mockapi_options_alter_mockapi_project"), + ] + + operations = [ + migrations.AddField( + model_name="mockapi", + name="api_desc", + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name="mockapi", + name="api_id", + field=models.CharField(default="4e9eb9a68bd8441d9c503f1347f156ff", max_length=32, unique=True), + ), + migrations.AddField( + model_name="mockapi", + name="api_name", + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name="mockapi", + name="followers", + field=models.JSONField(blank=True, default=list, null=True, verbose_name="关注者"), + ), + migrations.AlterField( + model_name="mockapi", + name="project", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="mock_apis", + to="mock.mockproject", + to_field="project_id", + ), + ), + ] diff --git a/mock/migrations/0004_remove_mockapi_followers_mockapi_enabled_and_more.py b/mock/migrations/0004_remove_mockapi_followers_mockapi_enabled_and_more.py new file mode 100644 index 00000000..71ba7d12 --- /dev/null +++ b/mock/migrations/0004_remove_mockapi_followers_mockapi_enabled_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.1.13 on 2024-03-02 11:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mock', '0003_mockapi_api_desc_mockapi_api_id_mockapi_api_name_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='mockapi', + name='followers', + ), + migrations.AddField( + model_name='mockapi', + name='enabled', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='mockapi', + name='api_id', + field=models.CharField(default='9b5855cb6dc945b1a7b8832d61a64069', max_length=32, unique=True), + ), + migrations.AlterField( + model_name='mockapi', + name='request_method', + field=models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PUT', 'PUT'), ('DELETE', 'DELETE'), ('PATCH', 'PATCH')], default='POST', max_length=10), + ), + migrations.CreateModel( + name='MockAPILog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('creator', models.CharField(max_length=20, null=True, verbose_name='创建人')), + ('updater', models.CharField(max_length=20, null=True, verbose_name='更新人')), + ('request_obj', models.JSONField(default=dict)), + ('response_obj', models.JSONField(default=dict)), + ('api', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='logs', to='mock.mockapi', to_field='api_id')), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='mock_logs', to='mock.mockproject', to_field='project_id')), + ], + options={ + 'verbose_name': 'mock api log表', + 'db_table': 'mock_api_log', + 'ordering': ['-create_time'], + }, + ), + ] diff --git a/mock/migrations/0005_mockapilog_request_id_alter_mockapi_api_id_and_more.py b/mock/migrations/0005_mockapilog_request_id_alter_mockapi_api_id_and_more.py new file mode 100644 index 00000000..be2698f0 --- /dev/null +++ b/mock/migrations/0005_mockapilog_request_id_alter_mockapi_api_id_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.13 on 2024-03-02 12:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mock', '0004_remove_mockapi_followers_mockapi_enabled_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='mockapilog', + name='request_id', + field=models.CharField(blank=True, db_index=True, default='bc73e83696024977b002016c6ebd9967', max_length=100, null=True), + ), + migrations.AlterField( + model_name='mockapi', + name='api_id', + field=models.CharField(default='28ee07527bf8431c955fdeaeb6f1626e', max_length=32, unique=True), + ), + migrations.AlterField( + model_name='mockapilog', + name='request_obj', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='mockapilog', + name='response_obj', + field=models.JSONField(blank=True, default=dict, null=True), + ), + ] diff --git a/mock/migrations/__init__.py b/mock/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mock/models.py b/mock/models.py new file mode 100644 index 00000000..c90d5d0b --- /dev/null +++ b/mock/models.py @@ -0,0 +1,96 @@ +import uuid + +from django.db import models + +from fastuser.models import BaseTable + + +class MockProject(BaseTable): + project_id = models.CharField(max_length=100, unique=True) + project_name = models.CharField(max_length=100) + project_desc = models.CharField(max_length=100) + is_active = models.BooleanField(default=True) + + class Meta: + verbose_name = "mock项目表" + db_table = "mock_project_tab" + unique_together = ["project_id"] + + +resp_text = """ +def execute(req, resp): + import requests + + url = "http://localhost:8000/api/mock/mock_api/" + + payload = {} + headers = { + "accept": "application/json", + "X-CSRFToken": "fk5wQDlKC6ufRjk7r38pfbqyq7mTtyc5NUUqkFN5lbZf6nyHVSbAUVoqbwaGcQHT", + } + + response = requests.request("GET", url, headers=headers, data=payload) + resp.data = response.json() +""" + + +class MockAPI(BaseTable): + METHOD_CHOICES = [ + ("GET", "GET"), + ("POST", "POST"), + ("PUT", "PUT"), + ("DELETE", "DELETE"), + ("PATCH", "PATCH"), + ] + + project = models.ForeignKey( + MockProject, + on_delete=models.DO_NOTHING, + db_constraint=False, + blank=True, + null=True, + to_field="project_id", + related_name="mock_apis", + ) + request_path = models.CharField(max_length=100) + request_method = models.CharField(max_length=10, choices=METHOD_CHOICES, default="POST") + request_body = models.JSONField(default=dict, blank=True, null=True) + response_text = models.TextField(default=resp_text) + is_active = models.BooleanField(default=True) + + api_name = models.CharField(max_length=100, null=True, blank=True) + api_desc = models.CharField(max_length=100, null=True, blank=True) + # uuid hex + api_id = models.CharField(max_length=32, default=lambda: uuid.uuid4().hex, unique=True) + enabled = models.BooleanField(default=True) + + # TODO 改成many to many + # followers: list = models.JSONField(null=True, blank=True, default=[], verbose_name="关注者") + + class Meta: + verbose_name = "mock接口表" + db_table = "mock_api_tab" + unique_together = ["project", "request_path", "request_method"] + ordering = ["-create_time"] + + +class MockAPILog(BaseTable): + api = models.ForeignKey(MockAPI, on_delete=models.DO_NOTHING, db_constraint=False, to_field='api_id', + related_name="logs") + project = models.ForeignKey( + MockProject, + on_delete=models.DO_NOTHING, + db_constraint=False, + blank=True, + null=True, + to_field="project_id", + related_name="mock_logs", + ) + request_obj = models.JSONField(default=dict, blank=True) + response_obj = models.JSONField(default=dict, null=True, blank=True) + request_id = models.CharField(max_length=100, default=uuid.uuid4().hex, db_index=True, null=True, blank=True) + + class Meta: + verbose_name = "mock api log表" + db_table = "mock_api_log" + ordering = ["-create_time"] diff --git a/mock/serializers.py b/mock/serializers.py new file mode 100644 index 00000000..d672ab3b --- /dev/null +++ b/mock/serializers.py @@ -0,0 +1,60 @@ +# !/usr/bin/python3 +# -*- coding: utf-8 -*- + +# @Author: 花菜 +# @File: serializers.py +# @Time : 2024/2/25 18:59 +# @Email: lihuacai168@gmail.com + +from rest_framework import serializers + +from .models import MockAPI, MockProject + + +class MockAPISerializer(serializers.ModelSerializer): + project_name = serializers.CharField(source="project.project_name", read_only=True) + + class Meta: + model = MockAPI + fields = [ + "id", + "project", + "project_name", + "request_path", + "request_method", + "request_body", + "response_text", + "is_active", + "api_id", + "api_desc", + "api_name", + "creator", + "updater", + "create_time", + "update_time", + ] + read_only_fields = [ + "id", + "api_id", + "creator", + "updater", + "create_time", + "update_time", + ] + + +class MockProjectSerializer(serializers.ModelSerializer): + class Meta: + model = MockProject + fields = [ + "id", + "project_id", + "project_name", + "project_desc", + "is_active", + "creator", + "updater", + "create_time", + "update_time", + ] + read_only_fields = ["id", "creator", "updater", "create_time", "update_time", "project_id"] diff --git a/mock/tests.py b/mock/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/mock/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mock/views.py b/mock/views.py new file mode 100644 index 00000000..978d949b --- /dev/null +++ b/mock/views.py @@ -0,0 +1,215 @@ +import json +import logging +import traceback +import types +import uuid + +from django_filters import rest_framework as filters +from django_filters.rest_framework import DjangoFilterBackend +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status, viewsets +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from FasterRunner.customer_swagger import CustomSwaggerAutoSchema +from .models import MockAPI, MockAPILog, MockProject +from .serializers import MockAPISerializer, MockProjectSerializer + +logger = logging.getLogger(__name__) + + +# mock function demo +def execute(req, resp): + import requests + + url = "http://localhost:8000/api/mock/mock_api/" + + payload = {} + headers = { + "accept": "application/json", + "X-CSRFToken": "fk5wQDlKC6ufRjk7r38pfbqyq7mTtyc5NUUqkFN5lbZf6nyHVSbAUVoqbwaGcQHT", + } + + response = requests.request("GET", url, headers=headers, data=payload) + resp.data = response.json() + + +class MockAPIFilter(filters.FilterSet): + project_name = filters.CharFilter(field_name='project__project_name', lookup_expr='icontains') + api_name = filters.CharFilter(lookup_expr="icontains") + request_path = filters.CharFilter(lookup_expr="icontains") + creator = filters.CharFilter(lookup_expr="exact") + + class Meta: + model = MockAPI + fields = ["project__project_name", "api_name", "creator", "request_path"] + +class MockAPIViewset(viewsets.ModelViewSet): + swagger_tag = '项目下的Mock API CRUD' + swagger_schema = CustomSwaggerAutoSchema + queryset = MockAPI.objects.all() + serializer_class = MockAPISerializer + filter_backends = [DjangoFilterBackend] + filterset_class = MockAPIFilter + +class RequestObject: + def __init__(self, request): + self.method: str = request.method + self.headers: dict = request.headers + self.raw_body: bytes = request.body + # 将字节对象解码为字符串,然后使用 json.loads() 方法将其转换为字典 + decoded_data = self.raw_body.decode("utf-8") + try: + self.body: dict = json.loads(decoded_data) + except Exception as e: + self.body = {} + self.query_params: dict = request.query_params + self.path: str = request._request.path + # Add any other properties you would like to ease access to + + +def dynamic_load_module(module_name, code): + # Create a new module with the given name + module = types.ModuleType(module_name) + try: + # Execute the code in the module's context + exec(code, module.__dict__) + # Return the created module + return module + except SyntaxError as e: + logger.error(f"SyntaxError during loading module {module_name}: {e}") + + +def load_and_execute(module_name, code, method_name, request) -> Response: + module = dynamic_load_module(module_name, code) + if module is not None: + try: + # Check if the method exists in the module + if hasattr(module, method_name): + # If the method exists, get it + method = getattr(module, method_name) + # Prepare a request object + req_obj: RequestObject = RequestObject(request) + # Execute method with request_object as an argument + + resp_obj = Response() + method(req_obj, resp_obj) + return resp_obj + else: + # If the method does not exist, log an error + logger.error( + f"The method {method_name} does not exist in the module {module_name}" + ) + data = {"error": f"Module should has {method_name} method"} + return Response(data=data) + except Exception as e: + # Generic catch-all for other errors during execution + logger.error( + f"Error executing the method {method_name} from module {module_name}: {e}\n {traceback.format_exc()}" + ) + + +def process(path, project_id, request: Request): + try: + logger.info(f"request path: {request.get_full_path()}") + + request_obj: dict = { + "method": request.method.upper(), + "path": path, + "mock_server_full_path": request.get_full_path(), + "body": request.data, + "headers": request.headers._store, + "query_params": request.query_params, + } + logger.debug(f"request_obj: {json.dumps(request_obj, indent=4)}") + mock_api = MockAPI.objects.get( + project__project_id=project_id, + request_path=path, + request_method=request.method.upper(), + is_active=True, + ) + request_id: str = uuid.uuid4().hex + log_obj = MockAPILog.objects.create( + request_obj=request_obj, + request_id=request_id, + api_id=mock_api.api_id, + project_id=mock_api.project, + ) + logger.debug(f"mock_api response_text\n{mock_api.response_text}") + response = load_and_execute( + "mock_api_module", mock_api.response_text, "execute", request + ) + response.headers.setdefault("X-Mock-RequestId", request_id) + response_obj = { + "status": response.status_code, + "body": response.data, + "headers": response.headers._store, + } + logger.debug(f"response_obj: {json.dumps(response_obj, indent=4)}") + log_obj.response_obj = response_obj + log_obj.save() + if response is not None: + return response + return Response({"error": "Execution failure"}) + except MockAPI.DoesNotExist: + logger.error( + f"Mock API does not exist for project_id: {project_id}, path: {path}, method: {request.method}" + ) + return Response({"error": "Mock API does not exist"}) + except Exception as e: + logger.error(f"Unhandled exception: {e}\n{traceback.format_exc()}") + return Response( + {"error": f"An unexpected error occurred, {traceback.format_exc()}"} + ) + + +class MockAPIView(APIView): + authentication_classes = [] + + @swagger_auto_schema(tags=["外部调用的mockapi"]) + def get(self, request: Request, project_id: str, path: str) -> Response: + return self.process_request(path, project_id, request) + + @swagger_auto_schema(tags=["外部调用的mockapi"]) + def post(self, request: Request, project_id: str, path: str) -> Response: + return self.process_request(path, project_id, request) + + @swagger_auto_schema(tags=["外部调用的mockapi"]) + def put(self, request: Request, project_id: str, path: str) -> Response: + return self.process_request(path, project_id, request) + + @swagger_auto_schema(tags=["外部调用的mockapi"]) + def delete(self, request: Request, project_id: str, path: str) -> Response: + return self.process_request(path, project_id, request) + + def process_request(self, path: str, project_id: str, request: Request) -> Response: + return process(path, project_id, request) + + +class MockProjectFilter(filters.FilterSet): + project_name = filters.CharFilter(lookup_expr="icontains") + project_desc = filters.CharFilter(lookup_expr="icontains") + creator = filters.CharFilter(lookup_expr="exact") + + class Meta: + model = MockProject + fields = ["project_name", "project_desc", "creator"] + + +class MockProjectViewSet(viewsets.ModelViewSet): + swagger_tag = 'Mock Project CRUD' + swagger_schema = CustomSwaggerAutoSchema + queryset = MockProject.objects.all() + serializer_class = MockProjectSerializer + filter_backends = [DjangoFilterBackend] + filterset_class = MockProjectFilter + + def create(self, request, *args, **kwargs): + data = request.data.copy() + data["project_id"] = str(uuid.uuid4().hex) + serializer = MockProjectSerializer(data=data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/web/package.json b/web/package.json index ffecb299..cfdf8950 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,9 @@ "build": "node build/build.js" }, "dependencies": { + "@babel/runtime": "^7.24.0", + "@femessage/el-data-table": "^1.23.2", + "@femessage/el-form-renderer": "^1.24.0", "ajv": "^6.12.6", "apexcharts": "^3.27.3", "axios": "^0.18.0", diff --git a/web/src/main.js b/web/src/main.js index cbac006f..676fcee0 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -16,6 +16,7 @@ import 'styles/reports.css' import * as api from './restful/api' import store from './store' import {datetimeObj2str, timestamp2time} from './util/format.js' + Vue.config.productionTip = false Vue.use(ElementUI) Vue.prototype.$api = api @@ -29,84 +30,140 @@ Vue.filter('datetimeFormat', datetimeObj2str) Vue.filter('timestampToTime', timestamp2time) -Vue.prototype.setLocalValue = function(name, value) { - if (window.localStorage) { - localStorage.setItem(name, value) - } else { - alert('This browser does NOT support localStorage') - } +Vue.prototype.setLocalValue = function (name, value) { + if (window.localStorage) { + localStorage.setItem(name, value) + } else { + alert('This browser does NOT support localStorage') + } } -Vue.prototype.getLocalValue = function(name) { - const value = localStorage.getItem(name) - if (value) { - // localStorage只能存字符串,布尔类型需要转换 - if (value === 'false' || value === 'true') { - return eval(value) +Vue.prototype.getLocalValue = function (name) { + const value = localStorage.getItem(name) + if (value) { + // localStorage只能存字符串,布尔类型需要转换 + if (value === 'false' || value === 'true') { + return eval(value) + } + return value + } else { + return '' } - return value - } else { - return '' - } } -router.beforeEach((to, from, next) => { - /* 路由发生变化修改页面title */ - setTimeout((res) => { - if (to.meta.title) { - document.title = to.meta.title - } -if (to.meta.requireAuth) { - if (store.state.token !== '') { - if (to.name === 'HomeRedirect' || to.name === 'Login') { - next({name: 'ProjectList'}); - } else { - next(); - } - } else { - next({ - name: 'Login' - }); - } -} else { - // 如果已经登录了,重新登录时,跳转到项目首页 - if(store.state.token !== '' && store.state.token !== null && store.state.token !== 'null' && to.name === 'Login'){ - next({name: 'ProjectList'}); - } else{ - next(); +// register component and loading directive +import ElDataTable from '@femessage/el-data-table' +import ElFormRenderer from '@femessage/el-form-renderer' +import { + Button, + Dialog, + Form, + FormItem, + Loading, + Pagination, + Table, + TableColumn, + Message, + MessageBox +} from 'element-ui' + +Vue.use(Button) +Vue.use(Dialog) +Vue.use(Form) +Vue.use(FormItem) +Vue.use(Loading.directive) +Vue.use(Pagination) +Vue.use(Table) +Vue.use(TableColumn) +Vue.component('el-form-renderer', ElFormRenderer) +Vue.component('el-data-table', ElDataTable) +// to show confirm before delete +Vue.prototype.$confirm = MessageBox.confirm +// show tips +Vue.prototype.$message = Message +// if the table component cannot access `this.$axios`, it cannot send request +import axios from 'axios' +// 创建axios实例 +const service = axios.create({ + // 可以在这里设置基础URL和其他配置 +}); + +// 请求拦截器 +service.interceptors.request.use( + config => { + config.headers.Authorization = store.state.token; + // 确保URL以斜杠结尾 + if (!config.url.endsWith('/')) { + config.url += '/'; + } + // 返回修改后的请求配置 + return config; + }, + error => { + // 请求错误处理 + return Promise.reject(error); } -} - }) +); +Vue.prototype.$axios = service + +router.beforeEach((to, from, next) => { + /* 路由发生变化修改页面title */ + setTimeout((res) => { + if (to.meta.title) { + document.title = to.meta.title + } + + if (to.meta.requireAuth) { + if (store.state.token !== '') { + if (to.name === 'HomeRedirect' || to.name === 'Login') { + next({name: 'ProjectList'}); + } else { + next(); + } + } else { + next({ + name: 'Login' + }); + } + } else { + // 如果已经登录了,重新登录时,跳转到项目首页 + if (store.state.token !== '' && store.state.token !== null && store.state.token !== 'null' && to.name === 'Login') { + next({name: 'ProjectList'}); + } else { + next(); + } + } + }) }) /* eslint-disable no-new */ new Vue({ - el: '#app', - router, - store, - components: {App}, - template: '', - created() { - if (this.getLocalValue('token') === null) { - this.setLocalValue('token', '') - } - if (this.getLocalValue('user') === null) { - this.setLocalValue('user', '') - } - if (this.getLocalValue('routerName') === null) { - this.setLocalValue('routerName', 'ProjectList') - } + el: '#app', + router, + store, + components: {App}, + template: '', + created() { + if (this.getLocalValue('token') === null) { + this.setLocalValue('token', '') + } + if (this.getLocalValue('user') === null) { + this.setLocalValue('user', '') + } + if (this.getLocalValue('routerName') === null) { + this.setLocalValue('routerName', 'ProjectList') + } - if (this.getLocalValue('is_superuser') === null) { - this.setLocalValue('is_superuser', false) - } - if (this.getLocalValue('show_hosts') === null) { - this.setLocalValue('show_hosts', false) + if (this.getLocalValue('is_superuser') === null) { + this.setLocalValue('is_superuser', false) + } + if (this.getLocalValue('show_hosts') === null) { + this.setLocalValue('show_hosts', false) + } + this.$store.commit('isLogin', this.getLocalValue('token')) + this.$store.commit('setUser', this.getLocalValue('user')) + this.$store.commit('setRouterName', this.getLocalValue('routerName')) + this.$store.commit('setIsSuperuser', this.getLocalValue('is_superuser')) + this.$store.commit('setShowHots', this.getLocalValue('show_hosts')) } - this.$store.commit('isLogin', this.getLocalValue('token')) - this.$store.commit('setUser', this.getLocalValue('user')) - this.$store.commit('setRouterName', this.getLocalValue('routerName')) - this.$store.commit('setIsSuperuser', this.getLocalValue('is_superuser')) - this.$store.commit('setShowHots', this.getLocalValue('show_hosts')) - } }) diff --git a/web/src/pages/common/layout/CommonLayout.vue b/web/src/pages/common/layout/CommonLayout.vue new file mode 100644 index 00000000..74da26c5 --- /dev/null +++ b/web/src/pages/common/layout/CommonLayout.vue @@ -0,0 +1,14 @@ + + + diff --git a/web/src/pages/home/components/Side.vue b/web/src/pages/home/components/Side.vue index 081f1bc4..3b8eb52d 100644 --- a/web/src/pages/home/components/Side.vue +++ b/web/src/pages/home/components/Side.vue @@ -1,5 +1,4 @@ diff --git a/web/src/pages/mock_server/mock_api/CustomAceEditor.vue b/web/src/pages/mock_server/mock_api/CustomAceEditor.vue new file mode 100644 index 00000000..0964ea3b --- /dev/null +++ b/web/src/pages/mock_server/mock_api/CustomAceEditor.vue @@ -0,0 +1,47 @@ +// CustomAceEditor.vue + + + + diff --git a/web/src/pages/mock_server/mock_api/index.vue b/web/src/pages/mock_server/mock_api/index.vue new file mode 100644 index 00000000..63c0532f --- /dev/null +++ b/web/src/pages/mock_server/mock_api/index.vue @@ -0,0 +1,416 @@ + + + diff --git a/web/src/pages/mock_server/mock_project/index.vue b/web/src/pages/mock_server/mock_project/index.vue new file mode 100644 index 00000000..5ce6c450 --- /dev/null +++ b/web/src/pages/mock_server/mock_project/index.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/web/src/restful/api.js b/web/src/restful/api.js index c3ff6d02..54b94299 100644 --- a/web/src/restful/api.js +++ b/web/src/restful/api.js @@ -407,3 +407,22 @@ export const getAllHost = url => { return axios.get('/api/fastrunner/host_ip/' + url + '/').then(res => res.data) }; +// create mock project, /api/mock/mock_project/' +export const createMockProject = data => { + return axios.post('/api/mock/mock_project/', data).then(res => res.data) +}; + +// get mock project list +export const getMockProject = params => { + return axios.get('/api/mock/mock_project/', params).then(res => res.data) +}; + + +// updateMockProject +export const updateMockProject = (project_id, data) => { + return axios.patch(`/api/mock/mock_project/${project_id}/`, data).then(res => res.data) +}; +// deleteMockProject +export const deleteMockProject = project_id => { + return axios.delete(`/api/mock/mock_project/${project_id}/`).then(res => res.data) +}; diff --git a/web/src/router/index.js b/web/src/router/index.js index a12fcec3..5205a776 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -13,6 +13,10 @@ import ReportList from '@/pages/reports/ReportList' import RecordConfig from '@/pages/fastrunner/config/RecordConfig' import Tasks from '@/pages/task/Tasks' import HostAddress from '@/pages/variables/HostAddress' +import MockProject from "@/pages/mock_server/mock_project/index.vue"; +import MockAPI from "@/pages/mock_server/mock_api/index.vue"; +import CommonLayout from "@/pages/common/layout/CommonLayout.vue"; + Vue.use(Router); @@ -32,7 +36,7 @@ export default new Router({ path: '/', name: 'HomeRedirect', redirect: '/fastrunner/login' - },{ + }, { path: '/fastrunner/login', name: 'Login', @@ -137,7 +141,6 @@ export default new Router({ title: '定时任务', requireAuth: true } - }, { name: 'HostIP', @@ -147,7 +150,35 @@ export default new Router({ title: 'HOST配置', requireAuth: true } - + }, + { + name: 'MockServer', + path: '/mock_server', + component: CommonLayout, + meta: { + title: 'MockServer', + requireAuth: true + }, + children: [ + { + name: 'MockProject', + path: 'mock_project/:id', + component: MockProject, + meta: { + title: 'Mock Project', + requireAuth: true + } + }, + { + name: 'MockAPIs', + path: 'mock_apis/:id', + component: MockAPI, + meta: { + title: 'Mock APIs', + requireAuth: true + } + } + ] } ] }, diff --git a/web/src/util/format.js b/web/src/util/format.js index 25a6eb04..13ab0afd 100644 --- a/web/src/util/format.js +++ b/web/src/util/format.js @@ -1,21 +1,18 @@ -export const datetimeObj2str = function (time, format = 'YY-MM-DD hh:mm:ss') { +export const datetimeObj2str = function (time, format = 'YYYY-MM-DD hh:mm:ss') { let date = new Date(time); let year = date.getFullYear(), - month = date.getMonth() + 1, - day = date.getDate(), - hour = date.getHours(), - min = date.getMinutes(), - sec = date.getSeconds(); - let preArr = Array.apply(null, Array(10)).map(function (elem, index) { - return '0' + index; - }); - - let newTime = format.replace(/YY/g, year) - .replace(/MM/g, preArr[month] || month) - .replace(/DD/g, preArr[day] || day) - .replace(/hh/g, preArr[hour] || hour) - .replace(/mm/g, preArr[min] || min) - .replace(/ss/g, preArr[sec] || sec); + month = (date.getMonth() + 1).toString().padStart(2, '0'), + day = date.getDate().toString().padStart(2, '0'), + hour = date.getHours().toString().padStart(2, '0'), + min = date.getMinutes().toString().padStart(2, '0'), + sec = date.getSeconds().toString().padStart(2, '0'); + + let newTime = format.replace(/YYYY/g, year) + .replace(/MM/g, month) + .replace(/DD/g, day) + .replace(/hh/g, hour) + .replace(/mm/g, min) + .replace(/ss/g, sec); return newTime; } diff --git a/web/yarn.lock b/web/yarn.lock index 98677666..eeff1a53 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -7,11 +7,44 @@ resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/runtime@^7.24.0": + version "7.24.0" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + dependencies: + regenerator-runtime "^0.14.0" + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@femessage/el-data-table@^1.23.2": + version "1.23.2" + resolved "https://registry.npmmirror.com/@femessage/el-data-table/-/el-data-table-1.23.2.tgz#cf2f269c5b207185b504265b88a9290667c28911" + integrity sha512-CDMnrLttO08bk2RaCSiO8OGtSecQx+4r9TdSSVq9wknvquiiQJq0lrIVeyOFmcGXUtmrZh1lhaAQqQRTzhnxqQ== + dependencies: + lodash.get "^4.4.2" + lodash.isempty "^4.4.0" + lodash.kebabcase "^4.1.1" + lodash.values "^4.3.0" + +"@femessage/el-form-renderer@^1.24.0": + version "1.24.0" + resolved "https://registry.npmmirror.com/@femessage/el-form-renderer/-/el-form-renderer-1.24.0.tgz#26ce54811a9287ce505554b9b8c5ab19781c4129" + integrity sha512-UDBBoRA9U4csGCjgWvf+Mcp/hLps34bPJWk4XJRL/I0c6F3tkB9B4q67rcDiPgukq7MSp3jbPz+pXnqKhz7Njw== + dependencies: + lodash.clonedeep "^4.5.0" + lodash.frompairs "^4.0.1" + lodash.get "^4.4.2" + lodash.has "^4.5.2" + lodash.includes "^4.3.0" + lodash.isequal "^4.5.0" + lodash.isplainobject "^4.0.6" + lodash.kebabcase "^4.1.1" + lodash.set "^4.3.2" + lodash.topairs "^4.3.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -4407,16 +4440,76 @@ lodash.camelcase@^4.3.0: resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.frompairs@^4.0.1: + version "4.0.1" + resolved "https://registry.npmmirror.com/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz#bc4e5207fa2757c136e573614e9664506b2b1bd2" + integrity sha512-dvqe2I+cO5MzXCMhUnfYFa9MD+/760yx2aTAN1lqEcEkf896TxgrX373igVdqSJj6tQd0jnSLE1UMuKufqqxFw== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + +lodash.has@^4.5.2: + version "4.5.2" + resolved "https://registry.npmmirror.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g== + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.kebabcase@^4.1.1: + version "4.1.1" + resolved "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.npmmirror.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== + +lodash.topairs@^4.3.0: + version "4.3.0" + resolved "https://registry.npmmirror.com/lodash.topairs/-/lodash.topairs-4.3.0.tgz#3b6deaa37d60fb116713c46c5f17ea190ec48d64" + integrity sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ== + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== +lodash.values@^4.3.0: + version "4.3.0" + resolved "https://registry.npmmirror.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + integrity sha512-r0RwvdCv8id9TUblb/O7rYPwVy6lerCbcawrfdo9iC/1t1wsNMJknO79WNBgwkH0hIeJ08jmvvESbFpNb4jH0Q== + lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4: version "4.17.21" resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -6277,6 +6370,11 @@ regenerator-runtime@^0.11.0: resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" @@ -6985,6 +7083,7 @@ strict-uri-encode@^1.0.0: integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7061,6 +7160,7 @@ string_decoder@~1.1.1: safe-buffer "~5.1.0" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7822,10 +7922,8 @@ watchpack@^1.4.0: resolved "https://registry.npmmirror.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^3.4.1" graceful-fs "^4.1.2" neo-async "^2.5.0" - watchpack-chokidar2 "^2.0.1" optionalDependencies: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1"